mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge v2.4.8
This commit is contained in:
commit
874acab90f
10
CHANGELOG.md
10
CHANGELOG.md
@ -80,13 +80,21 @@ NetBox now supports modeling physical cables for console, power, and interface c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
v2.4.8 (FUTURE)
|
v2.4.8 (2018-11-20)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2490](https://github.com/digitalocean/netbox/issues/2490) - Added bulk editing for config contexts
|
||||||
|
* [#2557](https://github.com/digitalocean/netbox/issues/2557) - Added object view for tags
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
* [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets
|
* [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets
|
||||||
* [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed
|
* [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed
|
||||||
|
* [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables
|
||||||
* [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table
|
* [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table
|
||||||
|
* [#2588](https://github.com/digitalocean/netbox/issues/2588) - Catch all exceptions from failed NAPALM API Calls
|
||||||
|
* [#2589](https://github.com/digitalocean/netbox/issues/2589) - Virtual machine API serializer should require cluster assignment
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -309,9 +309,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
# Check that NAPALM is installed
|
# Check that NAPALM is installed
|
||||||
try:
|
try:
|
||||||
import napalm
|
import napalm
|
||||||
|
from napalm.base.exceptions import ModuleImportError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
from napalm.base.exceptions import ModuleImportError
|
|
||||||
|
|
||||||
# Validate the configured driver
|
# Validate the configured driver
|
||||||
try:
|
try:
|
||||||
@ -355,7 +355,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
try:
|
try:
|
||||||
response[method] = getattr(d, method)()
|
response[method] = getattr(d, method)()
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
|
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||||
|
except Exception as e:
|
||||||
|
response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
|
||||||
d.close()
|
d.close()
|
||||||
|
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
@ -900,7 +900,7 @@ class DeviceView(View):
|
|||||||
interfaces = device.vc_interfaces.select_related(
|
interfaces = device.vc_interfaces.select_related(
|
||||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
|
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'cable__termination_a', 'cable__termination_b', 'ip_addresses'
|
'cable__termination_a', 'cable__termination_b', 'ip_addresses', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Front ports
|
# Front ports
|
||||||
|
@ -11,8 +11,8 @@ from taggit.models import Tag
|
|||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
||||||
JSONField, SlugField,
|
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||||
)
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
@ -206,6 +206,11 @@ class AddRemoveTagsForm(forms.Form):
|
|||||||
self.fields['remove_tags'] = TagField(required=False)
|
self.fields['remove_tags'] = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
model = Tag
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Config contexts
|
# Config contexts
|
||||||
#
|
#
|
||||||
@ -225,6 +230,28 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ConfigContext.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
min_value=0
|
||||||
|
)
|
||||||
|
is_active = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from taggit.models import Tag
|
from django_tables2.utils import Accessor
|
||||||
|
from taggit.models import Tag, TaggedItem
|
||||||
|
|
||||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||||
from .models import ConfigContext, ObjectChange
|
from .models import ConfigContext, ObjectChange
|
||||||
@ -13,6 +14,14 @@ TAG_ACTIONS = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TAGGED_ITEM = """
|
||||||
|
{% if value.get_absolute_url %}
|
||||||
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ value }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
CONFIGCONTEXT_ACTIONS = """
|
CONFIGCONTEXT_ACTIONS = """
|
||||||
{% if perms.extras.change_configcontext %}
|
{% if perms.extras.change_configcontext %}
|
||||||
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
@ -53,6 +62,10 @@ OBJECTCHANGE_REQUEST_ID = """
|
|||||||
|
|
||||||
class TagTable(BaseTable):
|
class TagTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.LinkColumn(
|
||||||
|
viewname='extras:tag',
|
||||||
|
args=[Accessor('slug')]
|
||||||
|
)
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=TAG_ACTIONS,
|
template_code=TAG_ACTIONS,
|
||||||
attrs={'td': {'class': 'text-right'}},
|
attrs={'td': {'class': 'text-right'}},
|
||||||
@ -64,6 +77,21 @@ class TagTable(BaseTable):
|
|||||||
fields = ('pk', 'name', 'items', 'slug', 'actions')
|
fields = ('pk', 'name', 'items', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemTable(BaseTable):
|
||||||
|
content_object = tables.TemplateColumn(
|
||||||
|
template_code=TAGGED_ITEM,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Object'
|
||||||
|
)
|
||||||
|
content_type = tables.Column(
|
||||||
|
verbose_name='Type'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = TaggedItem
|
||||||
|
fields = ('content_object', 'content_type')
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextTable(BaseTable):
|
class ConfigContextTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
|
@ -7,6 +7,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||||
|
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||||
@ -14,6 +15,7 @@ urlpatterns = [
|
|||||||
# Config contexts
|
# Config contexts
|
||||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||||
|
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -7,15 +8,20 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from taggit.models import Tag
|
from django_tables2 import RequestConfig
|
||||||
|
from taggit.models import Tag, TaggedItem
|
||||||
|
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
from utilities.paginator import EnhancedPaginator
|
||||||
|
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||||
from . import filters
|
from . import filters
|
||||||
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
from .forms import (
|
||||||
|
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
|
||||||
|
TagFilterForm, TagForm,
|
||||||
|
)
|
||||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||||
from .reports import get_report, get_reports
|
from .reports import get_report, get_reports
|
||||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -23,11 +29,45 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
|||||||
#
|
#
|
||||||
|
|
||||||
class TagListView(ObjectListView):
|
class TagListView(ObjectListView):
|
||||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
queryset = Tag.objects.annotate(
|
||||||
|
items=Count('taggit_taggeditem_items')
|
||||||
|
).order_by(
|
||||||
|
'name'
|
||||||
|
)
|
||||||
|
filter = filters.TagFilter
|
||||||
|
filter_form = TagFilterForm
|
||||||
table = TagTable
|
table = TagTable
|
||||||
template_name = 'extras/tag_list.html'
|
template_name = 'extras/tag_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class TagView(View):
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
|
||||||
|
tag = get_object_or_404(Tag, slug=slug)
|
||||||
|
tagged_items = TaggedItem.objects.filter(
|
||||||
|
tag=tag
|
||||||
|
).select_related(
|
||||||
|
'content_type'
|
||||||
|
).prefetch_related(
|
||||||
|
'content_object'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate a table of all items tagged with this Tag
|
||||||
|
items_table = TaggedItemTable(tagged_items)
|
||||||
|
paginate = {
|
||||||
|
'paginator_class': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(items_table)
|
||||||
|
|
||||||
|
return render(request, 'extras/tag.html', {
|
||||||
|
'tag': tag,
|
||||||
|
'items_count': tagged_items.count(),
|
||||||
|
'items_table': items_table,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'taggit.change_tag'
|
permission_required = 'taggit.change_tag'
|
||||||
model = Tag
|
model = Tag
|
||||||
@ -43,7 +83,11 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittype'
|
permission_required = 'circuits.delete_circuittype'
|
||||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
queryset = Tag.objects.annotate(
|
||||||
|
items=Count('taggit_taggeditem_items')
|
||||||
|
).order_by(
|
||||||
|
'name'
|
||||||
|
)
|
||||||
table = TagTable
|
table = TagTable
|
||||||
default_return_url = 'extras:tag_list'
|
default_return_url = 'extras:tag_list'
|
||||||
|
|
||||||
@ -83,6 +127,15 @@ class ConfigContextEditView(ConfigContextCreateView):
|
|||||||
permission_required = 'extras.change_configcontext'
|
permission_required = 'extras.change_configcontext'
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'extras.change_configcontext'
|
||||||
|
queryset = ConfigContext.objects.all()
|
||||||
|
filter = filters.ConfigContextFilter
|
||||||
|
table = ConfigContextTable
|
||||||
|
form = ConfigContextBulkEditForm
|
||||||
|
default_return_url = 'extras:configcontext_list'
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'extras.delete_configcontext'
|
permission_required = 'extras.delete_configcontext'
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
|
@ -390,6 +390,19 @@ table.report th a {
|
|||||||
top: -51px;
|
top: -51px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rendered Markdown */
|
||||||
|
.rendered-markdown table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.rendered-markdown th {
|
||||||
|
border-bottom: 2px solid #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.rendered-markdown td {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* AJAX loader */
|
/* AJAX loader */
|
||||||
.loading {
|
.loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if circuit.comments %}
|
{% if circuit.comments %}
|
||||||
{{ circuit.comments|gfm }}
|
{{ circuit.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if provider.comments %}
|
{% if provider.comments %}
|
||||||
{{ provider.comments|gfm }}
|
{{ provider.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -293,7 +293,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if device.comments %}
|
{% if device.comments %}
|
||||||
{{ device.comments|gfm }}
|
{{ device.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -508,6 +508,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>LAG</th>
|
<th>LAG</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
|
<th>MTU</th>
|
||||||
<th>Mode</th>
|
<th>Mode</th>
|
||||||
<th>Cable</th>
|
<th>Cable</th>
|
||||||
<th colspan="2">Connection</th>
|
<th colspan="2">Connection</th>
|
||||||
|
@ -116,7 +116,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if devicetype.comments %}
|
{% if devicetype.comments %}
|
||||||
{{ devicetype.comments|gfm }}
|
{{ devicetype.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{% load helpers %}
|
||||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
@ -13,17 +14,32 @@
|
|||||||
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
|
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
|
||||||
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
||||||
</span>
|
</span>
|
||||||
|
{% if iface.mac_address %}
|
||||||
|
<br/><small class="text-muted">{{ iface.mac_address }}</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# LAG #}
|
{# LAG #}
|
||||||
<td>
|
<td>
|
||||||
{% if iface.lag %}
|
{% if iface.lag %}
|
||||||
<a href="#iface_{{ iface.lag }}" class="label label-default" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
<a href="#interface_{{ iface.lag }}" class="label label-primary" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# Description #}
|
{# Description/tags #}
|
||||||
<td>{{ iface.description|default:"—" }}</td>
|
<td>
|
||||||
|
{% if iface.description %}
|
||||||
|
{{ iface.description }}<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% for tag in iface.tags.all %}
|
||||||
|
{% tag tag %}
|
||||||
|
{% empty %}
|
||||||
|
{% if not iface.description %}—{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# MTU #}
|
||||||
|
<td>{{ iface.mtu|default:"—" }}</td>
|
||||||
|
|
||||||
{# 802.1Q mode #}
|
{# 802.1Q mode #}
|
||||||
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||||
@ -44,7 +60,13 @@
|
|||||||
{% if iface.is_lag %}
|
{% if iface.is_lag %}
|
||||||
<td colspan="2" class="text-muted">
|
<td colspan="2" class="text-muted">
|
||||||
LAG interface<br />
|
LAG interface<br />
|
||||||
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
<small class="text-muted">
|
||||||
|
{% for member in iface.member_interfaces.all %}
|
||||||
|
<a href="#interface_{{ member.name }}">{{ member }}</a>{% if not forloop.last %}, {% endif %}
|
||||||
|
{% empty %}
|
||||||
|
No members
|
||||||
|
{% endfor %}
|
||||||
|
</small>
|
||||||
</td>
|
</td>
|
||||||
{% elif iface.is_virtual %}
|
{% elif iface.is_virtual %}
|
||||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
@ -138,7 +160,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# IP addresses table #}
|
{# IP addresses table #}
|
||||||
<td colspan="8" style="padding: 0">
|
<td colspan="9" style="padding: 0">
|
||||||
<table class="table table-condensed interface-ips">
|
<table class="table table-condensed interface-ips">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-muted">
|
<tr class="text-muted">
|
||||||
|
@ -182,7 +182,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if rack.comments %}
|
{% if rack.comments %}
|
||||||
{{ rack.comments|gfm }}
|
{{ rack.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -200,7 +200,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if site.comments %}
|
{% if site.comments %}
|
||||||
{{ site.comments|gfm }}
|
{{ site.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
69
netbox/templates/extras/tag.html
Normal file
69
netbox/templates/extras/tag.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8 col-md-9">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{% url 'extras:tag_list' %}">Tags</a></li>
|
||||||
|
<li>{{ tag }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 col-md-3">
|
||||||
|
<form action="{% url 'extras:tag_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
{% if perms.taggit.change_tag %}
|
||||||
|
<a href="{% url 'extras:tag_edit' slug=tag.slug %}?return_url={% url 'extras:tag' slug=tag.slug %}" class="btn btn-warning">
|
||||||
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||||
|
Edit this tag
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Tag</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>
|
||||||
|
{{ tag.name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Slug</td>
|
||||||
|
<td>
|
||||||
|
{{ tag.slug }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tagged Items</td>
|
||||||
|
<td>
|
||||||
|
{{ items_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -4,8 +4,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% block title %}Tags{% endblock %}</h1>
|
<h1>{% block title %}Tags{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if tenant.comments %}
|
{% if tenant.comments %}
|
||||||
{{ tenant.comments|gfm }}
|
{{ tenant.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if cluster.comments %}
|
{% if cluster.comments %}
|
||||||
{{ cluster.comments|gfm }}
|
{{ cluster.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body rendered-markdown">
|
||||||
{% if virtualmachine.comments %}
|
{% if virtualmachine.comments %}
|
||||||
{{ virtualmachine.comments|gfm }}
|
{{ virtualmachine.comments|gfm }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -52,7 +52,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||||
site = NestedSiteSerializer(read_only=True)
|
site = NestedSiteSerializer(read_only=True)
|
||||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
cluster = NestedClusterSerializer()
|
||||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
|
@ -378,6 +378,18 @@ class VirtualMachineTest(APITestCase):
|
|||||||
self.assertEqual(virtualmachine4.name, data['name'])
|
self.assertEqual(virtualmachine4.name, data['name'])
|
||||||
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
||||||
|
|
||||||
|
def test_create_virtualmachine_without_cluster(self):
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': 'Test Virtual Machine 4',
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse('virtualization-api:virtualmachine-list')
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(VirtualMachine.objects.count(), 3)
|
||||||
|
|
||||||
def test_create_virtualmachine_bulk(self):
|
def test_create_virtualmachine_bulk(self):
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
|
Loading…
Reference in New Issue
Block a user