mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'feature' into 9102-cabling
This commit is contained in:
@@ -196,6 +196,7 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
site = NestedSiteSerializer()
|
||||
parent = NestedLocationSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@@ -203,8 +204,8 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
@@ -297,7 +298,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT')
|
||||
)
|
||||
legend_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH
|
||||
)
|
||||
margin_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH
|
||||
)
|
||||
exclude = serializers.IntegerField(
|
||||
required=False,
|
||||
@@ -854,6 +858,8 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
||||
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -878,10 +884,10 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
|
||||
'cable', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
||||
'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'link_peers', 'link_peers_type',
|
||||
'wireless_lans', 'vrf', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
@@ -215,6 +215,14 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
data = serializer.validated_data
|
||||
|
||||
if data['render'] == 'svg':
|
||||
# Determine attributes for highlighting devices (if any)
|
||||
highlight_params = []
|
||||
for param in request.GET.getlist('highlight'):
|
||||
try:
|
||||
highlight_params.append(param.split(':', 1))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Render and return the elevation as an SVG drawing with the correct content type
|
||||
drawing = rack.get_elevation_svg(
|
||||
face=data['face'],
|
||||
@@ -223,7 +231,8 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
unit_height=data['unit_height'],
|
||||
legend_width=data['legend_width'],
|
||||
include_images=data['include_images'],
|
||||
base_url=request.build_absolute_uri('/')
|
||||
base_url=request.build_absolute_uri('/'),
|
||||
highlight_params=highlight_params
|
||||
)
|
||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||
|
||||
|
@@ -23,6 +23,28 @@ class SiteStatusChoices(ChoiceSet):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Locations
|
||||
#
|
||||
|
||||
class LocationStatusChoices(ChoiceSet):
|
||||
key = 'Location.status'
|
||||
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_STAGING = 'staging'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
STATUS_RETIRED = 'retired'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_STAGING, 'Staging', 'blue'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||
(STATUS_RETIRED, 'Retired', 'red'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -1003,6 +1025,51 @@ class InterfaceModeChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class InterfacePoEModeChoices(ChoiceSet):
|
||||
|
||||
MODE_PD = 'pd'
|
||||
MODE_PSE = 'pse'
|
||||
|
||||
CHOICES = (
|
||||
(MODE_PD, 'Powered device (PD)'),
|
||||
(MODE_PSE, 'Power sourcing equipment (PSE)'),
|
||||
)
|
||||
|
||||
|
||||
class InterfacePoETypeChoices(ChoiceSet):
|
||||
|
||||
TYPE_1_8023AF = 'type1-ieee802.3af'
|
||||
TYPE_2_8023AT = 'type2-ieee802.3at'
|
||||
TYPE_3_8023BT = 'type3-ieee802.3bt'
|
||||
TYPE_4_8023BT = 'type4-ieee802.3bt'
|
||||
|
||||
PASSIVE_24V_2PAIR = 'passive-24v-2pair'
|
||||
PASSIVE_24V_4PAIR = 'passive-24v-4pair'
|
||||
PASSIVE_48V_2PAIR = 'passive-48v-2pair'
|
||||
PASSIVE_48V_4PAIR = 'passive-48v-4pair'
|
||||
|
||||
CHOICES = (
|
||||
(
|
||||
'IEEE Standard',
|
||||
(
|
||||
(TYPE_1_8023AF, '802.3af (Type 1)'),
|
||||
(TYPE_2_8023AT, '802.3at (Type 2)'),
|
||||
(TYPE_3_8023BT, '802.3bt (Type 3)'),
|
||||
(TYPE_4_8023BT, '802.3bt (Type 4)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Passive',
|
||||
(
|
||||
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
|
||||
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
|
||||
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
|
||||
(PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# FrontPorts/RearPorts
|
||||
#
|
||||
|
@@ -13,7 +13,8 @@ DEVICETYPE_IMAGE_FORMATS = 'image/bmp,image/gif,image/jpeg,image/png,image/tiff,
|
||||
RACK_U_HEIGHT_DEFAULT = 42
|
||||
|
||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
||||
RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
|
||||
RACK_ELEVATION_DEFAULT_MARGIN_WIDTH = 15
|
||||
|
||||
|
||||
#
|
||||
|
@@ -217,10 +217,14 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
to_field_name='slug',
|
||||
label='Location (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=LocationStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ['id', 'name', 'slug', 'status', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1239,6 +1243,12 @@ class InterfaceFilterSet(
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter()
|
||||
wwn = MultiValueWWNFilter()
|
||||
poe_mode = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfacePoEModeChoices
|
||||
)
|
||||
poe_type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfacePoETypeChoices
|
||||
)
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
label='Assigned VLAN'
|
||||
@@ -1272,8 +1282,8 @@ class InterfaceFilterSet(
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
|
||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
|
||||
]
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
|
@@ -72,12 +72,15 @@ class PowerOutletBulkCreateForm(
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(
|
||||
form_from_model(Interface, ['type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected']),
|
||||
form_from_model(Interface, [
|
||||
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type',
|
||||
]),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = Interface
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
|
||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
|
||||
'poe_type', 'mark_connected', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
|
@@ -158,6 +158,12 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(LocationStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
@@ -169,7 +175,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Location
|
||||
fieldsets = (
|
||||
(None, ('site', 'parent', 'tenant', 'description')),
|
||||
(None, ('site', 'parent', 'status', 'tenant', 'description')),
|
||||
)
|
||||
nullable_fields = ('parent', 'tenant', 'description')
|
||||
|
||||
@@ -1063,6 +1069,18 @@ class InterfaceBulkEditForm(
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label='Management only'
|
||||
)
|
||||
poe_mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoEModeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoETypeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
@@ -1105,14 +1123,15 @@ class InterfaceBulkEditForm(
|
||||
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
|
||||
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vlan_group', 'untagged_vlan',
|
||||
'tagged_vlans', 'vrf',
|
||||
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@@ -124,6 +124,10 @@ class LocationCSVForm(NetBoxModelCSVForm):
|
||||
'invalid_choice': 'Location not found.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=LocationStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
@@ -133,7 +137,7 @@ class LocationCSVForm(NetBoxModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description')
|
||||
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
class RackRoleCSVForm(NetBoxModelCSVForm):
|
||||
@@ -622,6 +626,16 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
|
||||
choices=InterfaceDuplexChoices,
|
||||
required=False
|
||||
)
|
||||
poe_mode = CSVChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False,
|
||||
help_text='PoE mode'
|
||||
)
|
||||
poe_type = CSVChoiceField(
|
||||
choices=InterfacePoETypeChoices,
|
||||
required=False,
|
||||
help_text='PoE type'
|
||||
)
|
||||
mode = CSVChoiceField(
|
||||
choices=InterfaceModeChoices,
|
||||
required=False,
|
||||
@@ -642,9 +656,9 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'mac_address',
|
||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power',
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
||||
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
@@ -166,7 +166,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
model = Location
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
|
||||
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
@@ -198,6 +198,10 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
},
|
||||
label=_('Parent')
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
choices=LocationStatusChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -969,6 +973,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
@@ -1009,6 +1014,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
poe_mode = MultipleChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False
|
||||
)
|
||||
poe_type = MultipleChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False
|
||||
)
|
||||
rf_role = MultipleChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
|
@@ -194,7 +194,7 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Location', (
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tags',
|
||||
)),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
@@ -202,8 +202,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = (
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'tags',
|
||||
)
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class RackRoleForm(NetBoxModelForm):
|
||||
@@ -1314,6 +1318,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||
('Wireless', (
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
||||
@@ -1324,14 +1329,16 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans',
|
||||
'vrf', 'tags',
|
||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': SelectSpeedWidget(),
|
||||
'poe_mode': StaticSelect(),
|
||||
'poe_type': StaticSelect(),
|
||||
'duplex': StaticSelect(),
|
||||
'mode': StaticSelect(),
|
||||
'rf_role': StaticSelect(),
|
||||
|
@@ -234,6 +234,12 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType):
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
|
||||
def resolve_poe_mode(self, info):
|
||||
return self.poe_mode or None
|
||||
|
||||
def resolve_poe_type(self, info):
|
||||
return self.poe_type or None
|
||||
|
||||
def resolve_mode(self, info):
|
||||
return self.mode or None
|
||||
|
||||
|
23
netbox/dcim/migrations/0155_interface_poe_mode_type.py
Normal file
23
netbox/dcim/migrations/0155_interface_poe_mode_type.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-22 00:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0154_half_height_rack_units'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='poe_mode',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='poe_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
18
netbox/dcim/migrations/0156_location_status.py
Normal file
18
netbox/dcim/migrations/0156_location_status.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-22 17:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0155_interface_poe_mode_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
]
|
@@ -6,7 +6,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0154_half_height_rack_units'),
|
||||
('dcim', '0156_location_status'),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -40,7 +40,7 @@ def populate_cable_terminations(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0155_new_cabling_models'),
|
||||
('dcim', '0157_new_cabling_models'),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -39,7 +39,7 @@ def populate_cable_paths(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0156_populate_cable_terminations'),
|
||||
('dcim', '0158_populate_cable_terminations'),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -30,8 +30,8 @@ def populate_cable_terminations(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0036_new_cabling_models'),
|
||||
('dcim', '0157_populate_cable_paths'),
|
||||
('circuits', '0037_new_cabling_models'),
|
||||
('dcim', '0159_populate_cable_paths'),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -4,7 +4,7 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0158_populate_cable_ends'),
|
||||
('dcim', '0160_populate_cable_ends'),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -575,6 +575,18 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
||||
validators=(MaxValueValidator(127),),
|
||||
verbose_name='Transmit power (dBm)'
|
||||
)
|
||||
poe_mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoEModeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE mode'
|
||||
)
|
||||
poe_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoETypeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE type'
|
||||
)
|
||||
wireless_link = models.ForeignKey(
|
||||
to='wireless.WirelessLink',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -623,7 +635,7 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
||||
related_query_name='+'
|
||||
)
|
||||
|
||||
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
|
||||
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
@@ -711,6 +723,24 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
||||
f"of virtual chassis {self.device.virtual_chassis}."
|
||||
})
|
||||
|
||||
# PoE validation
|
||||
|
||||
# Only physical interfaces may have a PoE mode/type assigned
|
||||
if self.poe_mode and self.is_virtual:
|
||||
raise ValidationError({
|
||||
'poe_mode': "Virtual interfaces cannot have a PoE mode."
|
||||
})
|
||||
if self.poe_type and self.is_virtual:
|
||||
raise ValidationError({
|
||||
'poe_type': "Virtual interfaces cannot have a PoE type."
|
||||
})
|
||||
|
||||
# An interface with a PoE type set must also specify a mode
|
||||
if self.poe_type and not self.poe_mode:
|
||||
raise ValidationError({
|
||||
'poe_type': "Must specify PoE mode when designating a PoE type."
|
||||
})
|
||||
|
||||
# Wireless validation
|
||||
|
||||
# RF role & channel may only be set for wireless interfaces
|
||||
|
@@ -367,9 +367,11 @@ class Rack(NetBoxModel):
|
||||
user=None,
|
||||
unit_width=None,
|
||||
unit_height=None,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
legend_width=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH,
|
||||
margin_width=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH,
|
||||
include_images=True,
|
||||
base_url=None
|
||||
base_url=None,
|
||||
highlight_params=None
|
||||
):
|
||||
"""
|
||||
Return an SVG of the rack elevation
|
||||
@@ -381,6 +383,7 @@ class Rack(NetBoxModel):
|
||||
:param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
|
||||
height of the elevation
|
||||
:param legend_width: Width of the unit legend, in pixels
|
||||
:param margin_width: Width of the rigth-hand margin, in pixels
|
||||
:param include_images: Embed front/rear device images where available
|
||||
:param base_url: Base URL for links and images. If none, URLs will be relative.
|
||||
"""
|
||||
@@ -389,9 +392,11 @@ class Rack(NetBoxModel):
|
||||
unit_width=unit_width,
|
||||
unit_height=unit_height,
|
||||
legend_width=legend_width,
|
||||
margin_width=margin_width,
|
||||
user=user,
|
||||
include_images=include_images,
|
||||
base_url=base_url
|
||||
base_url=base_url,
|
||||
highlight_params=highlight_params
|
||||
)
|
||||
|
||||
return elevation.render(face)
|
||||
|
@@ -341,6 +341,11 @@ class Location(NestedGroupModel):
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
choices=LocationStatusChoices,
|
||||
default=LocationStatusChoices.STATUS_ACTIVE
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -367,7 +372,7 @@ class Location(NestedGroupModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
clone_fields = ['site', 'parent', 'tenant', 'description']
|
||||
clone_fields = ['site', 'parent', 'status', 'tenant', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@@ -409,6 +414,9 @@ class Location(NestedGroupModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return LocationStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@@ -7,12 +7,13 @@ from svgwrite.shapes import Rect
|
||||
from svgwrite.text import Text
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from netbox.config import get_config
|
||||
from utilities.utils import foreground_color
|
||||
from dcim.choices import DeviceFaceChoices
|
||||
from utilities.utils import foreground_color, array_to_ranges
|
||||
from dcim.constants import RACK_ELEVATION_BORDER_WIDTH
|
||||
|
||||
|
||||
@@ -51,12 +52,17 @@ class RackElevationSVG:
|
||||
Use this class to render a rack elevation as an SVG image.
|
||||
|
||||
:param rack: A NetBox Rack instance
|
||||
:param unit_width: Rendered unit width, in pixels
|
||||
:param unit_height: Rendered unit height, in pixels
|
||||
:param legend_width: Legend width, in pixels (where the unit labels appear)
|
||||
:param margin_width: Margin width, in pixels (where reservations appear)
|
||||
:param user: User instance. If specified, only devices viewable by this user will be fully displayed.
|
||||
:param include_images: If true, the SVG document will embed front/rear device face images, where available
|
||||
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
|
||||
:param highlight_params: Iterable of two-tuples which identifies attributes of devices to highlight
|
||||
"""
|
||||
def __init__(self, rack, unit_height=None, unit_width=None, legend_width=None, user=None, include_images=True,
|
||||
base_url=None):
|
||||
def __init__(self, rack, unit_height=None, unit_width=None, legend_width=None, margin_width=None, user=None,
|
||||
include_images=True, base_url=None, highlight_params=None):
|
||||
self.rack = rack
|
||||
self.include_images = include_images
|
||||
self.base_url = base_url.rstrip('/') if base_url is not None else ''
|
||||
@@ -65,7 +71,8 @@ class RackElevationSVG:
|
||||
config = get_config()
|
||||
self.unit_width = unit_width or config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
self.unit_height = unit_height or config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
self.legend_width = legend_width or config.RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
self.legend_width = legend_width or config.RACK_ELEVATION_DEFAULT_LEGEND_WIDTH
|
||||
self.margin_width = margin_width or config.RACK_ELEVATION_DEFAULT_MARGIN_WIDTH
|
||||
|
||||
# Determine the subset of devices within this rack that are viewable by the user, if any
|
||||
permitted_devices = self.rack.devices
|
||||
@@ -73,6 +80,17 @@ class RackElevationSVG:
|
||||
permitted_devices = permitted_devices.restrict(user, 'view')
|
||||
self.permitted_device_ids = permitted_devices.values_list('pk', flat=True)
|
||||
|
||||
# Determine device(s) to highlight within the elevation (if any)
|
||||
self.highlight_devices = []
|
||||
if highlight_params:
|
||||
q = Q()
|
||||
for k, v in highlight_params:
|
||||
q |= Q(**{k: v})
|
||||
try:
|
||||
self.highlight_devices = permitted_devices.filter(q)
|
||||
except FieldError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _add_gradient(drawing, id_, color):
|
||||
gradient = LinearGradient(
|
||||
@@ -91,7 +109,7 @@ class RackElevationSVG:
|
||||
drawing.defs.add(gradient)
|
||||
|
||||
def _setup_drawing(self):
|
||||
width = self.unit_width + self.legend_width + RACK_ELEVATION_BORDER_WIDTH * 2
|
||||
width = self.unit_width + self.legend_width + self.margin_width + RACK_ELEVATION_BORDER_WIDTH * 2
|
||||
height = self.unit_height * self.rack.u_height + RACK_ELEVATION_BORDER_WIDTH * 2
|
||||
drawing = svgwrite.Drawing(size=(width, height))
|
||||
|
||||
@@ -100,6 +118,7 @@ class RackElevationSVG:
|
||||
drawing.defs.add(drawing.style(css_file.read()))
|
||||
|
||||
# Add gradients
|
||||
RackElevationSVG._add_gradient(drawing, 'reserved', '#b0b0ff')
|
||||
RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
|
||||
RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
|
||||
|
||||
@@ -121,40 +140,44 @@ class RackElevationSVG:
|
||||
def _draw_device(self, device, coords, size, color=None, image=None):
|
||||
name = get_device_name(device)
|
||||
description = get_device_description(device)
|
||||
text_color = f'#{foreground_color(color)}' if color else '#000000'
|
||||
text_coords = (
|
||||
coords[0] + size[0] / 2,
|
||||
coords[1] + size[1] / 2
|
||||
)
|
||||
text_color = f'#{foreground_color(color)}' if color else '#000000'
|
||||
|
||||
# Determine whether highlighting is in use, and if so, whether to shade this device
|
||||
is_shaded = self.highlight_devices and device not in self.highlight_devices
|
||||
css_extra = ' shaded' if is_shaded else ''
|
||||
|
||||
# Create hyperlink element
|
||||
link = Hyperlink(
|
||||
href='{}{}'.format(
|
||||
self.base_url,
|
||||
reverse('dcim:device', kwargs={'pk': device.pk})
|
||||
),
|
||||
target='_blank',
|
||||
)
|
||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target='_blank')
|
||||
link.set_desc(description)
|
||||
|
||||
# Add rect element to hyperlink
|
||||
if color:
|
||||
link.add(Rect(coords, size, style=f'fill: #{color}', class_='slot'))
|
||||
link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
|
||||
else:
|
||||
link.add(Rect(coords, size, class_='slot blocked'))
|
||||
link.add(Text(name, insert=text_coords, fill=text_color))
|
||||
link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
|
||||
link.add(Text(name, insert=text_coords, fill=text_color, class_=f'label{css_extra}'))
|
||||
|
||||
# Embed device type image if provided
|
||||
if self.include_images and image:
|
||||
image = Image(
|
||||
href='{}{}'.format(self.base_url, image.url),
|
||||
href=f'{self.base_url}{image.url}',
|
||||
insert=coords,
|
||||
size=size,
|
||||
class_='device-image'
|
||||
class_=f'device-image{css_extra}'
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
link.add(image)
|
||||
link.add(Text(name, insert=text_coords, stroke='black',
|
||||
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
|
||||
link.add(Text(name, insert=text_coords, fill='white', class_='device-image-label'))
|
||||
link.add(
|
||||
Text(name, insert=text_coords, stroke='black', stroke_width='0.2em', stroke_linejoin='round',
|
||||
class_=f'device-image-label{css_extra}')
|
||||
)
|
||||
link.add(
|
||||
Text(name, insert=text_coords, fill='white', class_=f'device-image-label{css_extra}')
|
||||
)
|
||||
|
||||
self.drawing.add(link)
|
||||
|
||||
@@ -198,6 +221,29 @@ class RackElevationSVG:
|
||||
Text(str(unit), position_coordinates, class_='unit')
|
||||
)
|
||||
|
||||
def draw_margin(self):
|
||||
"""
|
||||
Draw any rack reservations in the right-hand margin alongside the rack elevation.
|
||||
"""
|
||||
for reservation in self.rack.reservations.all():
|
||||
for segment in array_to_ranges(reservation.units):
|
||||
u_height = 1 if len(segment) == 1 else segment[1] + 1 - segment[0]
|
||||
coords = self._get_device_coords(segment[0], u_height)
|
||||
coords = (coords[0] + self.unit_width + RACK_ELEVATION_BORDER_WIDTH * 2, coords[1])
|
||||
size = (
|
||||
self.margin_width,
|
||||
u_height * self.unit_height
|
||||
)
|
||||
link = Hyperlink(
|
||||
href='{}{}'.format(self.base_url, reservation.get_absolute_url()),
|
||||
target='_blank'
|
||||
)
|
||||
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
||||
link.add(
|
||||
Rect(coords, size, class_='reservation')
|
||||
)
|
||||
self.drawing.add(link)
|
||||
|
||||
def draw_background(self, face):
|
||||
"""
|
||||
Draw the rack unit placeholders which form the "background" of the rack elevation.
|
||||
@@ -261,16 +307,12 @@ class RackElevationSVG:
|
||||
# Initialize the drawing
|
||||
self.drawing = self._setup_drawing()
|
||||
|
||||
# Draw the empty rack & legend
|
||||
# Draw the empty rack, legend, and margin
|
||||
self.draw_legend()
|
||||
self.draw_background(face)
|
||||
self.draw_margin()
|
||||
|
||||
# Draw the opposite rack face first, then the near face
|
||||
if face == DeviceFaceChoices.FACE_REAR:
|
||||
opposite_face = DeviceFaceChoices.FACE_FRONT
|
||||
else:
|
||||
opposite_face = DeviceFaceChoices.FACE_REAR
|
||||
# self.draw_face(opposite_face, opposite=True)
|
||||
# Draw the rack face
|
||||
self.draw_face(face)
|
||||
|
||||
# Draw the rack border last
|
||||
|
@@ -520,10 +520,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
||||
'tagged_vlans', 'created', 'last_updated',
|
||||
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
|
||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
@@ -126,6 +126,7 @@ class LocationTable(NetBoxTable):
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
tenant = TenantColumn()
|
||||
rack_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:rack_list',
|
||||
@@ -150,7 +151,7 @@ class LocationTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Location
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
|
||||
'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'site', 'status', 'tenant', 'rack_count', 'device_count', 'description', 'slug',
|
||||
'contacts', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
||||
default_columns = ('pk', 'name', 'site', 'status', 'tenant', 'rack_count', 'device_count', 'description')
|
||||
|
@@ -197,13 +197,13 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_locations = (
|
||||
Location.objects.create(site=sites[0], name='Parent Location 1', slug='parent-location-1'),
|
||||
Location.objects.create(site=sites[1], name='Parent Location 2', slug='parent-location-2'),
|
||||
Location.objects.create(site=sites[0], name='Parent Location 1', slug='parent-location-1', status=LocationStatusChoices.STATUS_ACTIVE),
|
||||
Location.objects.create(site=sites[1], name='Parent Location 2', slug='parent-location-2', status=LocationStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 2', slug='location-2', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 3', slug='location-3', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1', parent=parent_locations[0], status=LocationStatusChoices.STATUS_ACTIVE)
|
||||
Location.objects.create(site=sites[0], name='Location 2', slug='location-2', parent=parent_locations[0], status=LocationStatusChoices.STATUS_ACTIVE)
|
||||
Location.objects.create(site=sites[0], name='Location 3', slug='location-3', parent=parent_locations[0], status=LocationStatusChoices.STATUS_ACTIVE)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
@@ -211,18 +211,21 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||
'slug': 'test-location-4',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
'status': LocationStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'name': 'Test Location 5',
|
||||
'slug': 'test-location-5',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
'status': LocationStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'name': 'Test Location 6',
|
||||
'slug': 'test-location-6',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
'status': LocationStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1507,6 +1510,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'speed': 1000000,
|
||||
'duplex': 'full',
|
||||
'vrf': vrfs[0].pk,
|
||||
'poe_mode': InterfacePoEModeChoices.MODE_PD,
|
||||
'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
|
@@ -265,9 +265,9 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
location.save()
|
||||
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], description='A'),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], description='B'),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], description='C'),
|
||||
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='A'),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='B'),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='C'),
|
||||
)
|
||||
for location in locations:
|
||||
location.save()
|
||||
@@ -280,6 +280,10 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['location-1', 'location-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [LocationStatusChoices.STATUS_PLANNED, LocationStatusChoices.STATUS_STAGING]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2540,14 +2544,109 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], module=modules[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First', vrf=vrfs[0], speed=1000000, duplex='half'),
|
||||
Interface(device=devices[1], module=modules[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second', vrf=vrfs[1], speed=1000000, duplex='full'),
|
||||
Interface(device=devices[2], module=modules[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third', vrf=vrfs[2], speed=100000, duplex='half'),
|
||||
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40, speed=100000, duplex='full'),
|
||||
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
|
||||
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40),
|
||||
Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22),
|
||||
Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_STATION, rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel_frequency=5160, rf_channel_width=20),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
module=modules[0],
|
||||
name='Interface 1',
|
||||
label='A',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_SFP,
|
||||
enabled=True,
|
||||
mgmt_only=True,
|
||||
mtu=100,
|
||||
mode=InterfaceModeChoices.MODE_ACCESS,
|
||||
mac_address='00-00-00-00-00-01',
|
||||
description='First',
|
||||
vrf=vrfs[0],
|
||||
speed=1000000,
|
||||
duplex='half',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
module=modules[1],
|
||||
name='Interface 2',
|
||||
label='B',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
enabled=True,
|
||||
mgmt_only=True,
|
||||
mtu=200,
|
||||
mode=InterfaceModeChoices.MODE_TAGGED,
|
||||
mac_address='00-00-00-00-00-02',
|
||||
description='Second',
|
||||
vrf=vrfs[1],
|
||||
speed=1000000,
|
||||
duplex='full',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
module=modules[2],
|
||||
name='Interface 3',
|
||||
label='C',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
enabled=False,
|
||||
mgmt_only=False,
|
||||
mtu=300,
|
||||
mode=InterfaceModeChoices.MODE_TAGGED_ALL,
|
||||
mac_address='00-00-00-00-00-03',
|
||||
description='Third',
|
||||
vrf=vrfs[2],
|
||||
speed=100000,
|
||||
duplex='half',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='Interface 4',
|
||||
label='D',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
enabled=True,
|
||||
mgmt_only=True,
|
||||
tx_power=40,
|
||||
speed=100000,
|
||||
duplex='full',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='Interface 5',
|
||||
label='E',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
enabled=True,
|
||||
mgmt_only=True,
|
||||
tx_power=40
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='Interface 6',
|
||||
label='F',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
enabled=False,
|
||||
mgmt_only=False,
|
||||
tx_power=40
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='Interface 7',
|
||||
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||
rf_role=WirelessRoleChoices.ROLE_AP,
|
||||
rf_channel=WirelessChannelChoices.CHANNEL_24G_1,
|
||||
rf_channel_frequency=2412,
|
||||
rf_channel_width=22
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='Interface 8',
|
||||
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||
rf_role=WirelessRoleChoices.ROLE_STATION,
|
||||
rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
|
||||
rf_channel_frequency=5160,
|
||||
rf_channel_width=20
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
@@ -2594,6 +2693,14 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'mgmt_only': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_poe_mode(self):
|
||||
params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_poe_type(self):
|
||||
params = {'poe_type': [InterfacePoETypeChoices.TYPE_1_8023AF, InterfacePoETypeChoices.TYPE_2_8023AT]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_mode(self):
|
||||
params = {'mode': InterfaceModeChoices.MODE_ACCESS}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
@@ -175,9 +175,9 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
|
||||
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=site, tenant=tenant),
|
||||
Location(name='Location 2', slug='location-2', site=site, tenant=tenant),
|
||||
Location(name='Location 3', slug='location-3', site=site, tenant=tenant),
|
||||
Location(name='Location 1', slug='location-1', site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant),
|
||||
Location(name='Location 2', slug='location-2', site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant),
|
||||
Location(name='Location 3', slug='location-3', site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant),
|
||||
)
|
||||
for location in locations:
|
||||
location.save()
|
||||
@@ -188,16 +188,17 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'name': 'Location X',
|
||||
'slug': 'location-x',
|
||||
'site': site.pk,
|
||||
'status': LocationStatusChoices.STATUS_PLANNED,
|
||||
'tenant': tenant.pk,
|
||||
'description': 'A new location',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,tenant,name,slug,description",
|
||||
"Site 1,Tenant 1,Location 4,location-4,Fourth location",
|
||||
"Site 1,Tenant 1,Location 5,location-5,Fifth location",
|
||||
"Site 1,Tenant 1,Location 6,location-6,Sixth location",
|
||||
"site,tenant,name,slug,status,description",
|
||||
"Site 1,Tenant 1,Location 4,location-4,planned,Fourth location",
|
||||
"Site 1,Tenant 1,Location 5,location-5,planned,Fifth location",
|
||||
"Site 1,Tenant 1,Location 6,location-6,planned,Sixth location",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -2204,6 +2205,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'description': 'A front port',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'tx_power': 10,
|
||||
'poe_mode': InterfacePoEModeChoices.MODE_PSE,
|
||||
'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||
@@ -2225,6 +2228,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'duplex': 'half',
|
||||
'mgmt_only': True,
|
||||
'description': 'A front port',
|
||||
'poe_mode': InterfacePoEModeChoices.MODE_PSE,
|
||||
'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
@@ -2244,6 +2249,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'duplex': 'full',
|
||||
'mgmt_only': True,
|
||||
'description': 'New description',
|
||||
'poe_mode': InterfacePoEModeChoices.MODE_PD,
|
||||
'poe_type': InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'tx_power': 10,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
@@ -2252,10 +2259,10 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
f"device,name,type,vrf.pk",
|
||||
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk}",
|
||||
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk}",
|
||||
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk}",
|
||||
f"device,name,type,vrf.pk,poe_mode,poe_type",
|
||||
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
|
@@ -639,6 +639,11 @@ class RackView(generic.ObjectView):
|
||||
|
||||
device_count = Device.objects.restrict(request.user, 'view').filter(rack=instance).count()
|
||||
|
||||
# Determine any additional parameters to pass when embedding the rack elevations
|
||||
svg_extra = '&'.join([
|
||||
f'highlight=id:{pk}' for pk in request.GET.getlist('device')
|
||||
])
|
||||
|
||||
return {
|
||||
'device_count': device_count,
|
||||
'reservations': reservations,
|
||||
@@ -646,6 +651,7 @@ class RackView(generic.ObjectView):
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
'svg_extra': svg_extra,
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user