mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 03:42:25 -06:00
Merge branch 'develop' into develop-2.7
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
# Circuit statuses
|
||||
CIRCUIT_STATUS_DEPROVISIONING = 0
|
||||
CIRCUIT_STATUS_ACTIVE = 1
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -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',
|
||||
[
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
57
netbox/dcim/migrations/0075_cable_devices.py
Normal file
57
netbox/dcim/migrations/0075_cable_devices.py
Normal 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
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Models which support custom fields
|
||||
CUSTOMFIELD_MODELS = [
|
||||
'circuits.circuit',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# IP address families
|
||||
AF_CHOICES = (
|
||||
(4, 'IPv4'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
2
netbox/project-static/js/jquery-3.3.1.min.js
vendored
2
netbox/project-static/js/jquery-3.3.1.min.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/js/jquery-3.4.1.min.js
vendored
Normal file
2
netbox/project-static/js/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,3 @@
|
||||
|
||||
class InvalidKey(Exception):
|
||||
"""
|
||||
Raised when a provided key is invalid.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user