mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-22 16:58:49 +02:00
This commit is contained in:
@@ -7,6 +7,18 @@ Device types are instantiated as devices installed within sites and/or equipment
|
|||||||
!!! note
|
!!! note
|
||||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device.
|
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device.
|
||||||
|
|
||||||
|
## Automatic Component Renaming
|
||||||
|
|
||||||
|
When adding component templates to a device type, the string `{vc_position}` can be used in component template names to reference the
|
||||||
|
`vc_position` field of the device being provisioned, when that device is a member of a Virtual Chassis.
|
||||||
|
|
||||||
|
For example, an interface template named `Gi{vc_position}/0/0` installed on a Virtual Chassis
|
||||||
|
member with position `2` will be rendered as `Gi2/0/0`.
|
||||||
|
|
||||||
|
If the device is not a member of a Virtual Chassis, `{vc_position}` defaults to `0`. A custom
|
||||||
|
fallback value can be specified using the syntax `{vc_position:X}`, where `X` is the desired default.
|
||||||
|
For example, `{vc_position:1}` will render as `1` when no Virtual Chassis position is set.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
### Manufacturer
|
### Manufacturer
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ When adding component templates to a module type, the string `{module}` can be u
|
|||||||
|
|
||||||
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
|
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
|
||||||
|
|
||||||
|
Similarly, the string `{vc_position}` can be used in component template names to reference the
|
||||||
|
`vc_position` field of the device being provisioned, when that device is a member of a Virtual Chassis.
|
||||||
|
|
||||||
|
For example, an interface template named `Gi{vc_position}/{module}/0` installed on a Virtual Chassis
|
||||||
|
member with position `2` and module bay position `3` will be rendered as `Gi2/3/0`.
|
||||||
|
|
||||||
|
If the device is not a member of a Virtual Chassis, `{vc_position}` defaults to `0`. A custom
|
||||||
|
fallback value can be specified using the syntax `{vc_position:X}`, where `X` is the desired default.
|
||||||
|
For example, `{vc_position:1}` will render as `1` when no Virtual Chassis position is set.
|
||||||
|
|
||||||
Automatic renaming is supported for all modular component types (those listed above).
|
Automatic renaming is supported for all modular component types (those listed above).
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from .choices import InterfaceTypeChoices
|
from .choices import InterfaceTypeChoices
|
||||||
@@ -79,6 +81,7 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
|||||||
#
|
#
|
||||||
|
|
||||||
MODULE_TOKEN = '{module}'
|
MODULE_TOKEN = '{module}'
|
||||||
|
VC_POSITION_RE = re.compile(r'\{vc_position(?::([^}]*))?\}')
|
||||||
|
|
||||||
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
||||||
app_label='dcim',
|
app_label='dcim',
|
||||||
|
|||||||
@@ -1072,7 +1072,9 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
|||||||
self.fields['name'].help_text = _(
|
self.fields['name'].help_text = _(
|
||||||
"Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
|
"Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
|
||||||
"supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
|
"supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
|
||||||
"automatically replaced with the position value when creating a new module."
|
"automatically replaced with the position value when creating a new module. "
|
||||||
|
"The token <code>{vc_position}</code> will be replaced with the device's Virtual Chassis position "
|
||||||
|
"(use <code>{vc_position:1}</code> to specify a fallback (default is 0))"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,26 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
_("A component template must be associated with either a device type or a module type.")
|
_("A component template must be associated with either a device type or a module type.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_vc_position(value: str, device) -> str:
|
||||||
|
"""
|
||||||
|
Resolves {vc_position} and {vc_position:X} tokens.
|
||||||
|
|
||||||
|
If the device has a vc_position, replaces the token with that value.
|
||||||
|
Otherwise uses the explicit fallback X if given, else '0'.
|
||||||
|
"""
|
||||||
|
def replacer(match):
|
||||||
|
explicit_fallback = match.group(1)
|
||||||
|
if (
|
||||||
|
device is not None
|
||||||
|
and device.virtual_chassis is not None
|
||||||
|
and device.vc_position is not None
|
||||||
|
):
|
||||||
|
return str(device.vc_position)
|
||||||
|
return explicit_fallback if explicit_fallback is not None else '0'
|
||||||
|
|
||||||
|
return VC_POSITION_RE.sub(replacer, value)
|
||||||
|
|
||||||
def _get_module_tree(self, module):
|
def _get_module_tree(self, module):
|
||||||
modules = []
|
modules = []
|
||||||
while module:
|
while module:
|
||||||
@@ -177,29 +197,42 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
modules.reverse()
|
modules.reverse()
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
def resolve_name(self, module):
|
def resolve_name(self, module=None, device=None):
|
||||||
if MODULE_TOKEN not in self.name:
|
has_module = MODULE_TOKEN in self.name
|
||||||
|
has_vc = VC_POSITION_RE.search(self.name) is not None
|
||||||
|
if not has_module and not has_vc:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
if module:
|
name = self.name
|
||||||
modules = self._get_module_tree(module)
|
|
||||||
name = self.name
|
|
||||||
for module in modules:
|
|
||||||
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
|
||||||
return name
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def resolve_label(self, module):
|
if has_module and module:
|
||||||
if MODULE_TOKEN not in self.label:
|
modules = self._get_module_tree(module)
|
||||||
|
for m in modules:
|
||||||
|
name = name.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||||
|
|
||||||
|
if has_vc:
|
||||||
|
resolved_device = (module.device if module else None) or device
|
||||||
|
name = self._resolve_vc_position(name, resolved_device)
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
def resolve_label(self, module=None, device=None):
|
||||||
|
has_module = MODULE_TOKEN in self.label
|
||||||
|
has_vc = VC_POSITION_RE.search(self.label) is not None
|
||||||
|
if not has_module and not has_vc:
|
||||||
return self.label
|
return self.label
|
||||||
|
|
||||||
if module:
|
label = self.label
|
||||||
|
|
||||||
|
if has_module and module:
|
||||||
modules = self._get_module_tree(module)
|
modules = self._get_module_tree(module)
|
||||||
label = self.label
|
for m in modules:
|
||||||
for module in modules:
|
label = label.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||||
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
if has_vc:
|
||||||
return label
|
resolved_device = (module.device if module else None) or device
|
||||||
return self.label
|
label = self._resolve_vc_position(label, resolved_device)
|
||||||
|
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||||
@@ -222,8 +255,8 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
@@ -257,8 +290,8 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
@@ -307,8 +340,8 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
maximum_draw=self.maximum_draw,
|
maximum_draw=self.maximum_draw,
|
||||||
allocated_draw=self.allocated_draw,
|
allocated_draw=self.allocated_draw,
|
||||||
@@ -395,13 +428,13 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
if self.power_port:
|
if self.power_port:
|
||||||
power_port_name = self.power_port.resolve_name(kwargs.get('module'))
|
power_port_name = self.power_port.resolve_name(kwargs.get('module'), kwargs.get('device'))
|
||||||
power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
|
power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
|
||||||
else:
|
else:
|
||||||
power_port = None
|
power_port = None
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
power_port=power_port,
|
power_port=power_port,
|
||||||
@@ -501,8 +534,8 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
enabled=self.enabled,
|
enabled=self.enabled,
|
||||||
mgmt_only=self.mgmt_only,
|
mgmt_only=self.mgmt_only,
|
||||||
@@ -628,8 +661,8 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
positions=self.positions,
|
positions=self.positions,
|
||||||
@@ -692,8 +725,8 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
positions=self.positions,
|
positions=self.positions,
|
||||||
@@ -731,8 +764,8 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
|||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||||
position=self.position,
|
position=self.position,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from netbox.config import ConfigItem
|
|||||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||||
from netbox.models.mixins import WeightMixin
|
from netbox.models.mixins import WeightMixin
|
||||||
|
from utilities.exceptions import AbortRequest
|
||||||
from utilities.fields import ColorField, CounterCacheField
|
from utilities.fields import ColorField, CounterCacheField
|
||||||
from utilities.prefetch import get_prefetchable_fields
|
from utilities.prefetch import get_prefetchable_fields
|
||||||
from utilities.tracking import TrackingModelMixin
|
from utilities.tracking import TrackingModelMixin
|
||||||
@@ -948,6 +949,20 @@ class Device(
|
|||||||
).format(virtual_chassis=self.vc_master_for)
|
).format(virtual_chassis=self.vc_master_for)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _check_duplicate_component_names(self, components):
|
||||||
|
"""
|
||||||
|
Check for duplicate component names after resolving {vc_position} placeholders.
|
||||||
|
Raises AbortRequest if duplicates are found.
|
||||||
|
"""
|
||||||
|
names = [c.name for c in components]
|
||||||
|
duplicates = {n for n in names if names.count(n) > 1}
|
||||||
|
if duplicates:
|
||||||
|
raise AbortRequest(
|
||||||
|
_("Component name conflict after resolving {{vc_position}}: {names}").format(
|
||||||
|
names=', '.join(duplicates)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _instantiate_components(self, queryset, bulk_create=True):
|
def _instantiate_components(self, queryset, bulk_create=True):
|
||||||
"""
|
"""
|
||||||
Instantiate components for the device from the specified component templates.
|
Instantiate components for the device from the specified component templates.
|
||||||
@@ -962,6 +977,10 @@ class Device(
|
|||||||
components = [obj.instantiate(device=self) for obj in queryset]
|
components = [obj.instantiate(device=self) for obj in queryset]
|
||||||
if not components:
|
if not components:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check for duplicate names after resolution {vc_position}
|
||||||
|
self._check_duplicate_component_names(components)
|
||||||
|
|
||||||
# Set default values for any applicable custom fields
|
# Set default values for any applicable custom fields
|
||||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||||
for component in components:
|
for component in components:
|
||||||
@@ -986,8 +1005,14 @@ class Device(
|
|||||||
update_fields=None
|
update_fields=None
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for obj in queryset:
|
components = [obj.instantiate(device=self) for obj in queryset]
|
||||||
component = obj.instantiate(device=self)
|
if not components:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for duplicate names after resolution {vc_position}
|
||||||
|
self._check_duplicate_component_names(components)
|
||||||
|
|
||||||
|
for component in components:
|
||||||
# Set default values for any applicable custom fields
|
# Set default values for any applicable custom fields
|
||||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||||
component.custom_field_data = cf_defaults
|
component.custom_field_data = cf_defaults
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from dcim.choices import (
|
|||||||
from dcim.forms import *
|
from dcim.forms import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
|
from utilities.exceptions import AbortRequest
|
||||||
from utilities.testing import create_test_device
|
from utilities.testing import create_test_device
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@@ -175,6 +176,88 @@ class DeviceTestCase(TestCase):
|
|||||||
self.assertIn('position', form.errors)
|
self.assertIn('position', form.errors)
|
||||||
|
|
||||||
|
|
||||||
|
class VCPositionTokenFormTestCase(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
Site.objects.create(name='Site VC 1', slug='site-vc-1')
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer VC 1', slug='manufacturer-vc-1')
|
||||||
|
device_type = DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Device Type VC 1', slug='device-type-vc-1'
|
||||||
|
)
|
||||||
|
DeviceRole.objects.create(name='Device Role VC 1', slug='device-role-vc-1', color='ff0000')
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='ge-{vc_position:0}/0/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
VirtualChassis.objects.create(name='VC 1')
|
||||||
|
|
||||||
|
def test_device_creation_in_vc_resolves_vc_position(self):
|
||||||
|
form = DeviceForm(data={
|
||||||
|
'name': 'Device VC Form 1',
|
||||||
|
'role': DeviceRole.objects.first().pk,
|
||||||
|
'tenant': None,
|
||||||
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
|
'device_type': DeviceType.objects.first().pk,
|
||||||
|
'site': Site.objects.first().pk,
|
||||||
|
'rack': None,
|
||||||
|
'face': None,
|
||||||
|
'position': None,
|
||||||
|
'platform': None,
|
||||||
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
|
'virtual_chassis': VirtualChassis.objects.first().pk,
|
||||||
|
'vc_position': 2,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
device = form.save()
|
||||||
|
self.assertTrue(device.interfaces.filter(name='ge-2/0/0').exists())
|
||||||
|
|
||||||
|
def test_device_creation_not_in_vc_uses_fallback(self):
|
||||||
|
form = DeviceForm(data={
|
||||||
|
'name': 'Device VC Form 2',
|
||||||
|
'role': DeviceRole.objects.first().pk,
|
||||||
|
'tenant': None,
|
||||||
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
|
'device_type': DeviceType.objects.first().pk,
|
||||||
|
'site': Site.objects.first().pk,
|
||||||
|
'rack': None,
|
||||||
|
'face': None,
|
||||||
|
'position': None,
|
||||||
|
'platform': None,
|
||||||
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
device = form.save()
|
||||||
|
self.assertTrue(device.interfaces.filter(name='ge-0/0/0').exists())
|
||||||
|
|
||||||
|
def test_device_creation_duplicate_name_conflict(self):
|
||||||
|
# With conflict
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
# to generate conflicts create an interface that will exist
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='ge-0/0/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
form = DeviceForm(data={
|
||||||
|
'name': 'Device VC Form 3',
|
||||||
|
'role': DeviceRole.objects.first().pk,
|
||||||
|
'tenant': None,
|
||||||
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
|
'device_type': device_type.pk,
|
||||||
|
'site': Site.objects.first().pk,
|
||||||
|
'rack': None,
|
||||||
|
'face': None,
|
||||||
|
'position': None,
|
||||||
|
'platform': None,
|
||||||
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
with self.assertRaises(AbortRequest):
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTestCase(TestCase):
|
class FrontPortTestCase(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1373,6 +1373,167 @@ class VirtualChassisTestCase(TestCase):
|
|||||||
device2.full_clean()
|
device2.full_clean()
|
||||||
|
|
||||||
|
|
||||||
|
class VCPositionTokenTestCase(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||||
|
DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||||
|
)
|
||||||
|
ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Test Module Type 1'
|
||||||
|
)
|
||||||
|
DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
|
||||||
|
|
||||||
|
def test_vc_position_token_in_vc(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
module_type=module_type,
|
||||||
|
name='ge-{vc_position}/{module}/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
vc = VirtualChassis.objects.create(name='Test VC 1')
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device VC 1', device_type=device_type, role=device_role,
|
||||||
|
site=site, virtual_chassis=vc, vc_position=8,
|
||||||
|
)
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
|
||||||
|
Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-8/1/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-8/1/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_not_in_vc_default_fallback(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
module_type=module_type,
|
||||||
|
name='ge-{vc_position}/{module}/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device NoVC 1', device_type=device_type, role=device_role,
|
||||||
|
site=site,
|
||||||
|
)
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
|
||||||
|
Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-0/1/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-0/1/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_explicit_fallback(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
module_type=module_type,
|
||||||
|
name='ge-{vc_position:18}/{module}/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device NoVC 2', device_type=device_type, role=device_role,
|
||||||
|
site=site,
|
||||||
|
)
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
|
||||||
|
Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-18/1/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-18/1/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_explicit_fallback_ignored_when_in_vc(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
module_type=module_type,
|
||||||
|
name='ge-{vc_position:99}/{module}/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
vc = VirtualChassis.objects.create(name='Test VC 2')
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device VC 2', device_type=device_type, role=device_role,
|
||||||
|
site=site, virtual_chassis=vc, vc_position=2,
|
||||||
|
)
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
|
||||||
|
Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-2/1/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-2/1/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_device_type_template(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='ge-{vc_position:0}/0/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
vc = VirtualChassis.objects.create(name='Test VC 3')
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device VC 3', device_type=device_type, role=device_role,
|
||||||
|
site=site, virtual_chassis=vc, vc_position=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-3/0/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-3/0/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_device_type_template_not_in_vc(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='ge-{vc_position:0}/0/0',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device NoVC 3', device_type=device_type, role=device_role,
|
||||||
|
site=site,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-0/0/0')
|
||||||
|
self.assertEqual(interface.name, 'ge-0/0/0')
|
||||||
|
|
||||||
|
def test_vc_position_token_label_resolution(self):
|
||||||
|
site = Site.objects.first()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
|
||||||
|
InterfaceTemplate.objects.create(
|
||||||
|
module_type=module_type,
|
||||||
|
name='ge-{vc_position}/{module}/0',
|
||||||
|
label='Member {vc_position:0} / Slot {module}',
|
||||||
|
type='1000base-t',
|
||||||
|
)
|
||||||
|
vc = VirtualChassis.objects.create(name='Test VC 4')
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device VC 4', device_type=device_type, role=device_role,
|
||||||
|
site=site, virtual_chassis=vc, vc_position=2,
|
||||||
|
)
|
||||||
|
module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
|
||||||
|
Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
|
||||||
|
|
||||||
|
interface = device.interfaces.get(name='ge-2/1/0')
|
||||||
|
self.assertEqual(interface.label, 'Member 2 / Slot 1')
|
||||||
|
|
||||||
|
|
||||||
class SiteSignalTestCase(TestCase):
|
class SiteSignalTestCase(TestCase):
|
||||||
|
|
||||||
@tag('regression')
|
@tag('regression')
|
||||||
|
|||||||
Reference in New Issue
Block a user