Merge branch 'develop' into develop-2.7

This commit is contained in:
Jeremy Stretch
2019-10-10 13:41:10 -04:00
86 changed files with 3001 additions and 2603 deletions

View File

@@ -1,4 +1,3 @@
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0
CIRCUIT_STATUS_ACTIVE = 1

View File

@@ -5,8 +5,8 @@ from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
from .constants import *
from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderFilter(CustomFieldFilterSet):

View File

@@ -3,14 +3,13 @@ from taggit.forms import TagField
from dcim.models import Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
)
from .constants import CIRCUIT_STATUS_CHOICES
from .constants import *
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -257,7 +256,8 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
required=False
)
comments = CommentField(
widget=SmallTextarea
widget=SmallTextarea,
label='Comments'
)
class Meta:

View File

@@ -9,7 +9,7 @@ from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
from .constants import *
class Provider(ChangeLoggedModel, CustomFieldModel):

View File

@@ -1,5 +1,4 @@
import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
@@ -11,7 +10,8 @@ CIRCUITTYPE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}"
class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@@ -58,14 +58,6 @@ SUBDEVICE_ROLE_CHOICES = (
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface ordering schemes (for device types)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Interface types
# Virtual
IFACE_TYPE_VIRTUAL = 0
@@ -120,6 +112,16 @@ IFACE_TYPE_8GFC_SFP_PLUS = 3080
IFACE_TYPE_16GFC_SFP_PLUS = 3160
IFACE_TYPE_32GFC_SFP28 = 3320
IFACE_TYPE_128GFC_QSFP28 = 3400
# InfiniBand
IFACE_FF_INFINIBAND_SDR = 7010
IFACE_FF_INFINIBAND_DDR = 7020
IFACE_FF_INFINIBAND_QDR = 7030
IFACE_FF_INFINIBAND_FDR10 = 7040
IFACE_FF_INFINIBAND_FDR = 7050
IFACE_FF_INFINIBAND_EDR = 7060
IFACE_FF_INFINIBAND_HDR = 7070
IFACE_FF_INFINIBAND_NDR = 7080
IFACE_FF_INFINIBAND_XDR = 7090
# Serial
IFACE_TYPE_T1 = 4000
IFACE_TYPE_E1 = 4010
@@ -222,6 +224,20 @@ IFACE_TYPE_CHOICES = [
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
'InfiniBand',
[
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
]
],
[
'Serial',
[

View File

@@ -1,6 +1,5 @@
import django_filters
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter
@@ -931,13 +930,28 @@ class CableFilter(django_filters.FilterSet):
color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES
)
device = django_filters.CharFilter(
method='filter_connected_device',
field_name='name'
device_id = MultiValueNumberFilter(
method='filter_device'
)
device_id = django_filters.CharFilter(
method='filter_connected_device',
field_name='pk'
device = MultiValueNumberFilter(
method='filter_device',
field_name='device__name'
)
rack_id = MultiValueNumberFilter(
method='filter_device',
field_name='device__rack_id'
)
rack = MultiValueNumberFilter(
method='filter_device',
field_name='device__rack__name'
)
site_id = MultiValueNumberFilter(
method='filter_device',
field_name='device__site_id'
)
site = MultiValueNumberFilter(
method='filter_device',
field_name='device__site__slug'
)
class Meta:
@@ -949,15 +963,12 @@ class CableFilter(django_filters.FilterSet):
return queryset
return queryset.filter(label__icontains=value)
def filter_connected_device(self, queryset, name, value):
if not value.strip():
return queryset
try:
device = Device.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
cable_pks = device.get_cables(pk_list=True)
return queryset.filter(pk__in=cable_pks)
def filter_device(self, queryset, name, value):
queryset = queryset.filter(
Q(**{'_termination_a_{}__in'.format(name): value}) |
Q(**{'_termination_b_{}__in'.format(name): value})
)
return queryset
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -17,8 +17,7 @@ from extras.forms import (
AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
)
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
@@ -57,6 +56,7 @@ def get_device_by_name_or_pk(name):
class InterfaceCommonForm:
def clean(self):
super().clean()
@@ -1023,6 +1023,16 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
'device_type': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType
if hasattr(self.instance, 'device_type'):
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=self.instance.device_type
)
class PowerOutletTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(
@@ -1107,6 +1117,16 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
'rear_port': StaticSelect2(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit rear_port choices to current DeviceType
if hasattr(self.instance, 'device_type'):
self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
device_type=self.instance.device_type
)
class FrontPortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(
@@ -2215,7 +2235,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(self.instance.device, 'site', None)
site = getattr(self.instance.parent, 'site', None)
if site is not None:
# Add non-grouped site VLANs
@@ -3112,6 +3132,26 @@ class CableFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
filter_for={
'rack_id': 'site',
}
)
)
rack_id = FilterChoiceField(
queryset=Rack.objects.all(),
label='Rack',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/racks/",
null_option=True,
)
)
type = forms.MultipleChoiceField(
choices=add_blank_choice(CABLE_TYPE_CHOICES),
required=False,

View File

@@ -0,0 +1,57 @@
from django.db import migrations, models
import django.db.models.deletion
def cache_cable_devices(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
print("\nUpdatng cable device terminations...")
cable_count = Cable.objects.count()
# Cache A/B termination devices on all existing Cables. Note that the custom save() method on Cable is not
# available during a migration, so we replicate its logic here.
for i, cable in enumerate(Cable.objects.all(), start=1):
if not i % 1000:
print("[{}/{}]".format(i, cable_count))
termination_a_model = apps.get_model(cable.termination_a_type.app_label, cable.termination_a_type.model)
termination_a_device = None
if hasattr(termination_a_model, 'device'):
termination_a = termination_a_model.objects.get(pk=cable.termination_a_id)
termination_a_device = termination_a.device
termination_b_model = apps.get_model(cable.termination_b_type.app_label, cable.termination_b_type.model)
termination_b_device = None
if hasattr(termination_b_model, 'device'):
termination_b = termination_b_model.objects.get(pk=cable.termination_b_id)
termination_b_device = termination_b.device
Cable.objects.filter(pk=cable.pk).update(
_termination_a_device=termination_a_device,
_termination_b_device=termination_b_device
)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0074_increase_field_length_platform_name_slug'),
]
operations = [
migrations.AddField(
model_name='cable',
name='_termination_a_device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'),
),
migrations.AddField(
model_name='cable',
name='_termination_b_device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'),
),
migrations.RunPython(
code=cache_cable_devices,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Case, Count, Q, Sum, When, F, Subquery, OuterRef
from django.db.models import Count, Q, Sum
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
@@ -1593,6 +1593,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
def clean(self):
super().clean()
# Validate site/rack combination
if self.rack and self.site != self.rack.site:
raise ValidationError({
@@ -2749,6 +2751,22 @@ class Cable(ChangeLoggedModel):
blank=True,
null=True
)
# Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by
# their associated Devices.
_termination_a_device = models.ForeignKey(
to=Device,
on_delete=models.CASCADE,
related_name='+',
blank=True,
null=True
)
_termination_b_device = models.ForeignKey(
to=Device,
on_delete=models.CASCADE,
related_name='+',
blank=True,
null=True
)
csv_headers = [
'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
@@ -2863,6 +2881,12 @@ class Cable(ChangeLoggedModel):
if self.length and self.length_unit:
self._abs_length = to_meters(self.length, self.length_unit)
# Store the parent Device for the A and B terminations (if applicable) to enable filtering
if hasattr(self.termination_a, 'device'):
self._termination_a_device = self.termination_a.device
if hasattr(self.termination_b, 'device'):
self._termination_b_device = self.termination_b.device
super().save(*args, **kwargs)
def to_csv(self):
@@ -3054,6 +3078,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
def to_csv(self):
return (
self.power_panel.site.name,
self.power_panel.name,
self.rack.name if self.rack else None,
self.name,

View File

@@ -195,6 +195,16 @@ POWERPANEL_POWERFEED_COUNT = """
"""
def get_component_template_actions(model_name):
return """
{{% if perms.dcim.change_{model_name} %}}
<a href="{{% url 'dcim:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{{% endif %}}
""".format(model_name=model_name).strip()
#
# Regions
#
@@ -404,74 +414,117 @@ class DeviceTypeTable(BaseTable):
class ConsolePortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleporttemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = ConsolePortTemplate
fields = ('pk', 'name')
fields = ('pk', 'name', 'actions')
empty_text = "None"
class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleserverporttemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
fields = ('pk', 'name', 'actions')
empty_text = "None"
class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('powerporttemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = PowerPortTemplate
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw')
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
empty_text = "None"
class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('poweroutlettemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = PowerOutletTemplate
fields = ('pk', 'name', 'power_port', 'feed_leg')
fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
empty_text = "None"
class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
actions = tables.TemplateColumn(
template_code=get_component_template_actions('interfacetemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = InterfaceTemplate
fields = ('pk', 'name', 'mgmt_only', 'type')
fields = ('pk', 'name', 'mgmt_only', 'type', 'actions')
empty_text = "None"
class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn()
rear_port_position = tables.Column(
verbose_name='Position'
)
actions = tables.TemplateColumn(
template_code=get_component_template_actions('frontporttemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = FrontPortTemplate
fields = ('pk', 'name', 'type', 'rear_port', 'rear_port_position')
fields = ('pk', 'name', 'type', 'rear_port', 'rear_port_position', 'actions')
empty_text = "None"
class RearPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('rearporttemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = RearPortTemplate
fields = ('pk', 'name', 'type', 'positions')
fields = ('pk', 'name', 'type', 'positions', 'actions')
empty_text = "None"
class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=get_component_template_actions('devicebaytemplate'),
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = DeviceBayTemplate
fields = ('pk', 'name')
fields = ('pk', 'name', 'actions')
empty_text = "None"

View File

@@ -93,35 +93,43 @@ urlpatterns = [
# Console port templates
path(r'device-types/<int:pk>/console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
path(r'device-types/<int:pk>/console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
path(r'console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
# Console server port templates
path(r'device-types/<int:pk>/console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
path(r'device-types/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
path(r'console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
# Power port templates
path(r'device-types/<int:pk>/power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
path(r'device-types/<int:pk>/power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
path(r'power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
# Power outlet templates
path(r'device-types/<int:pk>/power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
path(r'device-types/<int:pk>/power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
path(r'power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
# Interface templates
path(r'device-types/<int:pk>/interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
path(r'device-types/<int:pk>/interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
path(r'device-types/<int:pk>/interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
path(r'interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
# Front port templates
path(r'device-types/<int:pk>/front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
path(r'device-types/<int:pk>/front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
path(r'front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
# Rear port templates
path(r'device-types/<int:pk>/rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
path(r'device-types/<int:pk>/rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
path(r'rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
# Device bay templates
path(r'device-types/<int:pk>/device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
path(r'device-types/<int:pk>/device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
path(r'device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
# Device roles
path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),

View File

@@ -691,6 +691,12 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
template_name = 'dcim/device_component_add.html'
class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_consoleporttemplate'
model = ConsolePortTemplate
model_form = forms.ConsolePortTemplateForm
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleporttemplate'
queryset = ConsolePortTemplate.objects.all()
@@ -708,6 +714,12 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
template_name = 'dcim/device_component_add.html'
class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_consoleserverporttemplate'
model = ConsoleServerPortTemplate
model_form = forms.ConsoleServerPortTemplateForm
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate'
queryset = ConsoleServerPortTemplate.objects.all()
@@ -725,6 +737,12 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
template_name = 'dcim/device_component_add.html'
class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_powerporttemplate'
model = PowerPortTemplate
model_form = forms.PowerPortTemplateForm
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
queryset = PowerPortTemplate.objects.all()
@@ -742,6 +760,12 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
template_name = 'dcim/device_component_add.html'
class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_poweroutlettemplate'
model = PowerOutletTemplate
model_form = forms.PowerOutletTemplateForm
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate'
queryset = PowerOutletTemplate.objects.all()
@@ -759,6 +783,12 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
template_name = 'dcim/device_component_add.html'
class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_interfacetemplate'
model = InterfaceTemplate
model_form = forms.InterfaceTemplateForm
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interfacetemplate'
queryset = InterfaceTemplate.objects.all()
@@ -784,6 +814,12 @@ class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
template_name = 'dcim/device_component_add.html'
class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_frontporttemplate'
model = FrontPortTemplate
model_form = forms.FrontPortTemplateForm
class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontporttemplate'
queryset = FrontPortTemplate.objects.all()
@@ -801,6 +837,12 @@ class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
template_name = 'dcim/device_component_add.html'
class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rearporttemplate'
model = RearPortTemplate
model_form = forms.RearPortTemplateForm
class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearporttemplate'
queryset = RearPortTemplate.objects.all()
@@ -818,6 +860,12 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
template_name = 'dcim/device_component_add.html'
class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicebaytemplate'
model = DeviceBayTemplate
model_form = forms.DeviceBayTemplateForm
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
queryset = DeviceBayTemplate.objects.all()

View File

@@ -86,6 +86,10 @@ class CustomLinkForm(forms.ModelForm):
class Meta:
model = CustomLink
exclude = []
widgets = {
'text': forms.Textarea,
'url': forms.Textarea,
}
help_texts = {
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
'which render as empty text will not be displayed.',

View File

@@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
from extras.constants import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer

View File

@@ -1,4 +1,3 @@
# Models which support custom fields
CUSTOMFIELD_MODELS = [
'circuits.circuit',

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag

View File

@@ -8,16 +8,12 @@ from taggit.forms import TagField
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.constants import COLOR_CHOICES
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
)
from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
OBJECTCHANGE_ACTION_CHOICES,
)
from .constants import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@@ -431,7 +427,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
help_text="Commit changes to the database (uncheck for a dry-run)"
)
def __init__(self, vars, *args, **kwargs):
def __init__(self, vars, *args, commit_default=True, **kwargs):
super().__init__(*args, **kwargs)
@@ -439,6 +435,10 @@ class ScriptForm(BootstrapMixin, forms.Form):
for name, var in vars.items():
self.fields[name] = var.as_field()
# Toggle default commit behavior based on Meta option
if not commit_default:
self.fields['_commit'].initial = False
# Move _commit to the end of the form
self.fields.move_to_end('_commit', True)

View File

@@ -9,9 +9,7 @@ from django.utils import timezone
from django.utils.functional import curry
from django_prometheus.models import model_deletes, model_inserts, model_updates
from .constants import (
OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE,
)
from .constants import *
from .models import ObjectChange
from .signals import purge_changelog
from .webhooks import enqueue_webhooks

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from datetime import date
import graphviz
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -654,7 +655,10 @@ class ConfigContext(models.Model):
class ConfigContextModel(models.Model):
"""
A model which includes local configuration context data. This local data will override any inherited data from
ConfigContexts.
"""
local_context_data = JSONField(
blank=True,
null=True,
@@ -679,6 +683,16 @@ class ConfigContextModel(models.Model):
return data
def clean(self):
super().clean()
# Verify that JSON data is provided as an object
if self.local_context_data and type(self.local_context_data) is not dict:
raise ValidationError(
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
)
#
# Custom scripts

View File

@@ -6,7 +6,7 @@ from collections import OrderedDict
from django.conf import settings
from django.utils import timezone
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
from .constants import *
from .models import ReportResult

View File

@@ -1,17 +1,17 @@
from collections import OrderedDict
import inspect
import json
import os
import pkgutil
import time
import traceback
import yaml
from collections import OrderedDict
import yaml
from django import forms
from django.conf import settings
from django.core.validators import RegexValidator
from django.db import transaction
from mptt.forms import TreeNodeChoiceField
from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
from mptt.models import MPTTModel
from ipam.formfields import IPFormField
@@ -21,13 +21,13 @@ from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARN
from .forms import ScriptForm
from .signals import purge_changelog
__all__ = [
'BaseScript',
'BooleanVar',
'FileVar',
'IntegerVar',
'IPNetworkVar',
'MultiObjectVar',
'ObjectVar',
'Script',
'StringVar',
@@ -150,6 +150,23 @@ class ObjectVar(ScriptVariable):
self.form_field = TreeNodeChoiceField
class MultiObjectVar(ScriptVariable):
"""
Like ObjectVar, but can represent one or more objects.
"""
form_field = forms.ModelMultipleChoiceField
def __init__(self, queryset, *args, **kwargs):
super().__init__(*args, **kwargs)
# Queryset for field choices
self.field_attrs['queryset'] = queryset
# Update form field for MPTT (nested) objects
if issubclass(queryset.model, MPTTModel):
self.form_field = TreeNodeMultipleChoiceField
class FileVar(ScriptVariable):
"""
An uploaded file.
@@ -226,7 +243,7 @@ class BaseScript:
Return a Django form suitable for populating the context data required to run this Script.
"""
vars = self._get_vars()
form = ScriptForm(vars, data, files)
form = ScriptForm(vars, data, files, commit_default=getattr(self.Meta, 'commit_default', True))
return form

View File

@@ -120,6 +120,29 @@ class ScriptVariablesTest(TestCase):
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['var1'].pk, data['var1'])
def test_multiobjectvar(self):
class TestScript(Script):
var1 = MultiObjectVar(
queryset=DeviceRole.objects.all()
)
# Populate some objects
for i in range(1, 6):
DeviceRole(
name='Device Role {}'.format(i),
slug='device-role-{}'.format(i)
).save()
# Validate valid data
data = {'var1': [role.pk for role in DeviceRole.objects.all()[:3]]}
form = TestScript().as_form(data, None)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['var1'][0].pk, data['var1'][0])
self.assertEqual(form.cleaned_data['var1'][1].pk, data['var1'][1])
self.assertEqual(form.cleaned_data['var1'][2].pk, data['var1'][2])
def test_filevar(self):
class TestScript(Script):

View File

@@ -3,10 +3,9 @@ import datetime
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
from extras.models import Webhook
from utilities.api import get_serializer_for_model
from .constants import WEBHOOK_MODELS
from .constants import *
def enqueue_webhooks(instance, user, request_id, action):

View File

@@ -6,7 +6,7 @@ import requests
from django_rq import job
from rest_framework.utils.encoders import JSONEncoder
from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJECTCHANGE_ACTION_CHOICES
from .constants import *
@job('default')

View File

@@ -1,4 +1,3 @@
# IP address families
AF_CHOICES = (
(4, 'IPv4'),

View File

@@ -9,7 +9,7 @@ from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF

View File

@@ -5,8 +5,7 @@ from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
@@ -14,9 +13,7 @@ from utilities.forms import (
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
)
from virtualization.models import VirtualMachine
from .constants import (
IP_PROTOCOL_CHOICES, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
)
from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
IP_FAMILY_CHOICES = [

View File

@@ -1,5 +1,4 @@
from django.db.models import Lookup, Transform, IntegerField
from django.db.models import lookups
from django.db.models import IntegerField, Lookup, Transform, lookups
class NetFieldDecoratorMixin(object):

View File

@@ -9,10 +9,11 @@ from django.db.models.expressions import RawSQL
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import Interface
from dcim.models import Device, Interface
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from virtualization.models import VirtualMachine
from .constants import *
from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet
@@ -636,6 +637,34 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
)
})
if self.pk:
# Check for primary IP assignment that doesn't match the assigned device/VM
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if device:
if self.interface is None:
raise ValidationError({
'interface': "IP address is primary for device {} but not assigned".format(device)
})
elif (device.primary_ip4 == self or device.primary_ip6 == self) and self.interface.device != device:
raise ValidationError({
'interface': "IP address is primary for device {} but assigned to {} ({})".format(
device, self.interface.device, self.interface
)
})
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if vm:
if self.interface is None:
raise ValidationError({
'interface': "IP address is primary for virtual machine {} but not assigned".format(vm)
})
elif (vm.primary_ip4 == self or vm.primary_ip6 == self) and self.interface.virtual_machine != vm:
raise ValidationError({
'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format(
vm, self.interface.virtual_machine, self.interface
)
})
def save(self, *args, **kwargs):
# Record address family

View File

@@ -6,7 +6,7 @@ class PrefixQuerySet(QuerySet):
def annotate_depth(self, limit=None):
"""
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
to do this using .annotate() on the QuerySet to count the unique parents of each prefix, that approach introduces
performance issues at scale.
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet

View File

@@ -2,6 +2,7 @@ import netaddr
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q
from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View
from django_tables2 import RequestConfig
@@ -13,7 +14,7 @@ from utilities.views import (
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@@ -291,9 +292,10 @@ class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class AggregateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_aggregate'
queryset = Aggregate.objects.prefetch_related('rir').extra(select={
'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
})
queryset = Aggregate.objects.prefetch_related('rir').annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
)
filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable

View File

@@ -40,43 +40,54 @@ SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict((
# Circuits
('provider', {
'permission': 'circuits.view_provider',
'queryset': Provider.objects.all(),
'filter': ProviderFilter,
'table': ProviderTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.prefetch_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'permission': 'circuits.view_circuit',
'queryset': Circuit.objects.prefetch_related(
'type', 'provider', 'tenant'
).prefetch_related(
'terminations__site'
),
'filter': CircuitFilter,
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
# DCIM
('site', {
'permission': 'dcim.view_site',
'queryset': Site.objects.prefetch_related('region', 'tenant'),
'filter': SiteFilter,
'table': SiteTable,
'url': 'dcim:site_list',
}),
('rack', {
'permission': 'dcim.view_rack',
'queryset': Rack.objects.prefetch_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter,
'table': RackTable,
'url': 'dcim:rack_list',
}),
('rackgroup', {
'permission': 'dcim.view_rackgroup',
'queryset': RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')),
'filter': RackGroupFilter,
'table': RackGroupTable,
'url': 'dcim:rackgroup_list',
}),
('devicetype', {
'permission': 'dcim.view_devicetype',
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')),
'filter': DeviceTypeFilter,
'table': DeviceTypeTable,
'url': 'dcim:devicetype_list',
}),
('device', {
'permission': 'dcim.view_device',
'queryset': Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
),
@@ -85,18 +96,21 @@ SEARCH_TYPES = OrderedDict((
'url': 'dcim:device_list',
}),
('virtualchassis', {
'permission': 'dcim.view_virtualchassis',
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members')),
'filter': VirtualChassisFilter,
'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
('cable', {
'permission': 'dcim.view_cable',
'queryset': Cable.objects.all(),
'filter': CableFilter,
'table': CableTable,
'url': 'dcim:cable_list',
}),
('powerfeed', {
'permission': 'dcim.view_powerfeed',
'queryset': PowerFeed.objects.all(),
'filter': PowerFeedFilter,
'table': PowerFeedTable,
@@ -104,30 +118,35 @@ SEARCH_TYPES = OrderedDict((
}),
# IPAM
('vrf', {
'permission': 'ipam.view_vrf',
'queryset': VRF.objects.prefetch_related('tenant'),
'filter': VRFFilter,
'table': VRFTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'permission': 'ipam.view_aggregate',
'queryset': Aggregate.objects.prefetch_related('rir'),
'filter': AggregateFilter,
'table': AggregateTable,
'url': 'ipam:aggregate_list',
}),
('prefix', {
'permission': 'ipam.view_prefix',
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter,
'table': PrefixTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'permission': 'ipam.view_ipaddress',
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
'filter': IPAddressFilter,
'table': IPAddressTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'permission': 'ipam.view_vlan',
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter,
'table': VLANTable,
@@ -135,6 +154,7 @@ SEARCH_TYPES = OrderedDict((
}),
# Secrets
('secret', {
'permission': 'secrets.view_secret',
'queryset': Secret.objects.prefetch_related('role', 'device'),
'filter': SecretFilter,
'table': SecretTable,
@@ -142,6 +162,7 @@ SEARCH_TYPES = OrderedDict((
}),
# Tenancy
('tenant', {
'permission': 'tenancy.view_tenant',
'queryset': Tenant.objects.prefetch_related('group'),
'filter': TenantFilter,
'table': TenantTable,
@@ -149,12 +170,14 @@ SEARCH_TYPES = OrderedDict((
}),
# Virtualization
('cluster', {
'permission': 'virtualization.view_cluster',
'queryset': Cluster.objects.prefetch_related('type', 'group'),
'filter': ClusterFilter,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'permission': 'virtualization.view_virtualmachine',
'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
),
@@ -243,11 +266,16 @@ class SearchView(View):
if form.is_valid():
# Searching for a single type of object
obj_types = []
if form.cleaned_data['obj_type']:
obj_types = [form.cleaned_data['obj_type']]
obj_type = form.cleaned_data['obj_type']
if request.user.has_perm(SEARCH_TYPES[obj_type]['permission']):
obj_types.append(form.cleaned_data['obj_type'])
# Searching all object types
else:
obj_types = SEARCH_TYPES.keys()
for obj_type in SEARCH_TYPES.keys():
if request.user.has_perm(SEARCH_TYPES[obj_type]['permission']):
obj_types.append(obj_type)
for obj_type in obj_types:

View File

@@ -149,6 +149,9 @@ table.attr-table td:nth-child(1) {
.table-headings th {
background-color: #f5f5f5;
}
td.min-width {
width: 1%;
}
/* Paginator */
div.paginator {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
class InvalidKey(Exception):
"""
Raised when a provided key is invalid.

View File

@@ -65,7 +65,7 @@
</div>
</div>
</footer>
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/jquery-3.4.1.min.js' %}"></script>
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
<script src="{% static 'select2-4.0.5/js/select2.min.js' %}"></script>

View File

@@ -94,8 +94,11 @@ $(document).ready(function() {
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
$("#cpu").after(row)
});
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
if (json['get_environment']['memory']) {
var memory = $('#memory');
memory.after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
memory.after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
}
$.each(json['get_environment']['temperature'], function(name, obj) {
var style = "success";
if (obj['is_alert']) {

View File

@@ -5,10 +5,8 @@
{% block content %}
{% if obj %}<h1>{{ obj }}</h1>{% endif %}
{% include 'panel_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% if settings.CHANGELOG_RETENTION %}
<div class="text-muted">
Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
</div>
{% endif %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
<div class="text-muted">
Changelog retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
</div>
{% endblock %}

View File

@@ -9,11 +9,9 @@
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' %}
{% if settings.CHANGELOG_RETENTION %}
<div class="pull-right text-muted">
Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
</div>
{% endif %}
<div class="text-muted text-right">
Changelog retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
</div>
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}

View File

@@ -24,7 +24,7 @@
</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">
<a href="{% url 'extras:tag_edit' slug=tag.slug %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
Edit this tag
</a>

View File

@@ -1,4 +1,10 @@
{% extends 'rest_framework/base.html' %}
{% load static %}
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
{% block branding %}
<a class="navbar-brand" href="/{{ settings.BASE_PATH }}">NetBox</a>

View File

@@ -1,4 +1,5 @@
import django_filters
from .models import Tenant, TenantGroup

View File

@@ -6,7 +6,6 @@ from django.contrib.auth.models import User
from netbox.admin import admin_site
from .models import Token
# Unregister the built-in UserAdmin so that we can use our custom admin view below
admin_site.unregister(User)

View File

@@ -6,10 +6,9 @@ from rest_framework.relations import ManyRelatedField
from taggit_serializer.serializers import TagListSerializerField
from dcim.api.serializers import InterfaceSerializer as DeviceInterfaceSerializer
from virtualization.api.serializers import InterfaceSerializer as VirtualMachineInterfaceSerializer
from extras.api.customfields import CustomFieldsSerializer
from utilities.api import ChoiceField, SerializedPKRelatedField, WritableNestedSerializer
from virtualization.api.serializers import InterfaceSerializer as VirtualMachineInterfaceSerializer
# this might be ugly, but it limits drf_yasg-specific code to this file
DeviceInterfaceSerializer.Meta.ref_name = 'DeviceInterface'

View File

@@ -6,8 +6,6 @@ from io import StringIO
from django import forms
from django.conf import settings
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
from django.db.models import Count
from django.urls import reverse_lazy
from mptt.forms import TreeNodeMultipleChoiceField
from .constants import *

View File

@@ -1,4 +1,5 @@
from django.db.models import Manager
from django.db.models.expressions import RawSQL
NAT1 = r"CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)"
NAT2 = r"SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')"
@@ -21,11 +22,11 @@ class NaturalOrderingManager(Manager):
db_field = self.natural_order_field
# Append the three subfields derived from the designated natural ordering field
queryset = queryset.extra(select={
'_nat1': NAT1.format(db_table, db_field),
'_nat2': NAT2.format(db_table, db_field),
'_nat3': NAT3.format(db_table, db_field),
})
queryset = (
queryset.annotate(_nat1=RawSQL(NAT1.format(db_table, db_field), ()))
.annotate(_nat2=RawSQL(NAT2.format(db_table, db_field), ()))
.annotate(_nat3=RawSQL(NAT3.format(db_table, db_field), ()))
)
# Replace any instance of the designated natural ordering field with its three subfields
ordering = []

View File

@@ -1,8 +1,9 @@
from urllib import parse
from django.conf import settings
from django.db import ProgrammingError
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
import urllib
from .views import server_error
@@ -26,7 +27,7 @@ class LoginRequiredMiddleware(object):
return HttpResponseRedirect(
'{}?next={}'.format(
settings.LOGIN_URL,
urllib.parse.quote(request.get_full_path_info())
parse.quote(request.get_full_path_info())
)
)
return self.get_response(request)

View File

@@ -26,6 +26,12 @@ class ToggleColumn(tables.CheckBoxColumn):
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', '')
visible = kwargs.pop('visible', False)
if 'attrs' not in kwargs:
kwargs['attrs'] = {
'td': {
'class': 'min-width'
}
}
super().__init__(*args, default=default, visible=visible, **kwargs)
@property

View File

@@ -3,6 +3,7 @@ import json
import re
from django import template
from django.utils.html import strip_tags
from django.utils.safestring import mark_safe
from markdown import markdown
@@ -58,7 +59,12 @@ def gfm(value):
"""
Render text as GitHub-Flavored Markdown
"""
# Strip HTML tags
value = strip_tags(value)
# Render Markdown with GFM extension
html = markdown(value, extensions=['mdx_gfm'])
return mark_safe(html)

View File

@@ -1,7 +1,6 @@
from collections import OrderedDict
import datetime
import json
from collections import OrderedDict
from django.core.serializers import serialize
from django.db.models import Count, OuterRef, Subquery

View File

@@ -9,7 +9,7 @@ from tenancy.filtersets import TenancyFilterSet
from utilities.filters import (
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
)
from .constants import VM_STATUS_CHOICES
from .constants import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine

View File

@@ -7,8 +7,7 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
from ipam.models import IPAddress, VLANGroup, VLAN
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
@@ -16,7 +15,7 @@ from utilities.forms import (
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
SmallTextarea, StaticSelect2, StaticSelect2Multiple
)
from .constants import VM_STATUS_CHOICES
from .constants import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
VIFACE_TYPE_CHOICES = (
@@ -671,7 +670,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(self.instance.device, 'site', None)
site = getattr(self.instance.parent, 'site', None)
if site is not None:
# Add non-grouped site VLANs

View File

@@ -8,7 +8,7 @@ from taggit.managers import TaggableManager
from dcim.models import Device
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
from utilities.models import ChangeLoggedModel
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
from .constants import *
#
@@ -255,6 +255,8 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
def clean(self):
super().clean()
# Validate primary IP addresses
interfaces = self.interfaces.all()
for field in ['primary_ip4', 'primary_ip6']: