mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-19 07:54:45 +01:00
This commit is contained in:
@@ -7,6 +7,18 @@ Device types are instantiated as devices installed within sites and/or equipment
|
||||
!!! 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.
|
||||
|
||||
## 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
|
||||
|
||||
### 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]`.
|
||||
|
||||
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).
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .choices import InterfaceTypeChoices
|
||||
@@ -79,6 +81,7 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
#
|
||||
|
||||
MODULE_TOKEN = '{module}'
|
||||
VC_POSITION_RE = re.compile(r'\{vc_position(?::([^}]*))?\}')
|
||||
|
||||
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
||||
app_label='dcim',
|
||||
|
||||
@@ -1072,7 +1072,9 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
self.fields['name'].help_text = _(
|
||||
"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 "
|
||||
"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.")
|
||||
)
|
||||
|
||||
@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):
|
||||
modules = []
|
||||
while module:
|
||||
@@ -177,29 +197,42 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
modules.reverse()
|
||||
return modules
|
||||
|
||||
def resolve_name(self, module):
|
||||
if MODULE_TOKEN not in self.name:
|
||||
def resolve_name(self, module=None, device=None):
|
||||
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
|
||||
|
||||
if module:
|
||||
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
|
||||
name = self.name
|
||||
|
||||
def resolve_label(self, module):
|
||||
if MODULE_TOKEN not in self.label:
|
||||
if has_module and module:
|
||||
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
|
||||
|
||||
if module:
|
||||
label = self.label
|
||||
|
||||
if has_module and module:
|
||||
modules = self._get_module_tree(module)
|
||||
label = self.label
|
||||
for module in modules:
|
||||
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
||||
return label
|
||||
return self.label
|
||||
for m in modules:
|
||||
label = label.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||
if has_vc:
|
||||
resolved_device = (module.device if module else None) or device
|
||||
label = self._resolve_vc_position(label, resolved_device)
|
||||
|
||||
return label
|
||||
|
||||
|
||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
@@ -222,8 +255,8 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -257,8 +290,8 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -307,8 +340,8 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
maximum_draw=self.maximum_draw,
|
||||
allocated_draw=self.allocated_draw,
|
||||
@@ -395,13 +428,13 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
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)
|
||||
else:
|
||||
power_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
power_port=power_port,
|
||||
@@ -501,8 +534,8 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
enabled=self.enabled,
|
||||
mgmt_only=self.mgmt_only,
|
||||
@@ -628,8 +661,8 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
positions=self.positions,
|
||||
@@ -692,8 +725,8 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
positions=self.positions,
|
||||
@@ -731,8 +764,8 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
position=self.position,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ from netbox.config import ConfigItem
|
||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.fields import ColorField, CounterCacheField
|
||||
from utilities.prefetch import get_prefetchable_fields
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
@@ -948,6 +949,20 @@ class Device(
|
||||
).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):
|
||||
"""
|
||||
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]
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Check for duplicate names after resolution {vc_position}
|
||||
self._check_duplicate_component_names(components)
|
||||
|
||||
# Set default values for any applicable custom fields
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
for component in components:
|
||||
@@ -986,8 +1005,14 @@ class Device(
|
||||
update_fields=None
|
||||
)
|
||||
else:
|
||||
for obj in queryset:
|
||||
component = obj.instantiate(device=self)
|
||||
components = [obj.instantiate(device=self) for obj in queryset]
|
||||
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
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
component.custom_field_data = cf_defaults
|
||||
|
||||
@@ -11,6 +11,7 @@ from dcim.choices import (
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.testing import create_test_device
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@@ -175,6 +176,88 @@ class DeviceTestCase(TestCase):
|
||||
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):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1373,6 +1373,167 @@ class VirtualChassisTestCase(TestCase):
|
||||
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):
|
||||
|
||||
@tag('regression')
|
||||
|
||||
Reference in New Issue
Block a user