mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 04:56:29 -06:00
Merge pull request #7611 from netbox-community/3979-wireless
Closes #3979: Wireless network modeling
This commit is contained in:
commit
334c97035e
8
docs/core-functionality/wireless.md
Normal file
8
docs/core-functionality/wireless.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Wireless Networks
|
||||||
|
|
||||||
|
{!models/wireless/wirelesslan.md!}
|
||||||
|
{!models/wireless/wirelesslangroup.md!}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{!models/wireless/wirelesslink.md!}
|
@ -11,6 +11,17 @@ Interfaces may be physical or virtual in nature, but only physical interfaces ma
|
|||||||
|
|
||||||
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
||||||
|
|
||||||
|
### Wireless Interfaces
|
||||||
|
|
||||||
|
Wireless interfaces may additionally track the following attributes:
|
||||||
|
|
||||||
|
* **Role** - AP or station
|
||||||
|
* **Channel** - One of several standard wireless channels
|
||||||
|
* **Channel Frequency** - The transmit frequency
|
||||||
|
* **Channel Width** - Channel bandwidth
|
||||||
|
|
||||||
|
If a predefined channel is selected, the frequency and width attributes will be assigned automatically. If no channel is selected, these attributes may be defined manually.
|
||||||
|
|
||||||
### IP Address Assignment
|
### IP Address Assignment
|
||||||
|
|
||||||
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||||
|
11
docs/models/wireless/wirelesslan.md
Normal file
11
docs/models/wireless/wirelesslan.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Wireless LANs
|
||||||
|
|
||||||
|
A wireless LAN is a set of interfaces connected via a common wireless channel. Each instance must have an SSID, and may optionally be correlated to a VLAN. Wireless LANs can be arranged into hierarchical groups.
|
||||||
|
|
||||||
|
An interface may be attached to multiple wireless LANs, provided they are all operating on the same channel. Only wireless interfaces may be attached to wireless LANs.
|
||||||
|
|
||||||
|
Each wireless LAN may have authentication attributes associated with it, including:
|
||||||
|
|
||||||
|
* Authentication type
|
||||||
|
* Cipher
|
||||||
|
* Pre-shared key
|
3
docs/models/wireless/wirelesslangroup.md
Normal file
3
docs/models/wireless/wirelesslangroup.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Wireless LAN Groups
|
||||||
|
|
||||||
|
Wireless LAN groups can be used to organize and classify wireless LANs. These groups are hierarchical: groups can be nested within parent groups. However, each wireless LAN may assigned only to one group.
|
9
docs/models/wireless/wirelesslink.md
Normal file
9
docs/models/wireless/wirelesslink.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Wireless Links
|
||||||
|
|
||||||
|
A wireless link represents a connection between exactly two wireless interfaces. It may optionally be assigned an SSID and a description. It may also have a status assigned to it, similar to the cable model.
|
||||||
|
|
||||||
|
Each wireless link may have authentication attributes associated with it, including:
|
||||||
|
|
||||||
|
* Authentication type
|
||||||
|
* Cipher
|
||||||
|
* Pre-shared key
|
@ -60,6 +60,7 @@ nav:
|
|||||||
- Virtualization: 'core-functionality/virtualization.md'
|
- Virtualization: 'core-functionality/virtualization.md'
|
||||||
- Service Mapping: 'core-functionality/services.md'
|
- Service Mapping: 'core-functionality/services.md'
|
||||||
- Circuits: 'core-functionality/circuits.md'
|
- Circuits: 'core-functionality/circuits.md'
|
||||||
|
- Wireless: 'core-functionality/wireless.md'
|
||||||
- Power Tracking: 'core-functionality/power.md'
|
- Power Tracking: 'core-functionality/power.md'
|
||||||
- Tenancy: 'core-functionality/tenancy.md'
|
- Tenancy: 'core-functionality/tenancy.md'
|
||||||
- Contacts: 'core-functionality/contacts.md'
|
- Contacts: 'core-functionality/contacts.md'
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
|||||||
from circuits.choices import CircuitStatusChoices
|
from circuits.choices import CircuitStatusChoices
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import CableTerminationSerializer
|
from dcim.api.serializers import LinkTerminationSerializer
|
||||||
from netbox.api import ChoiceField
|
from netbox.api import ChoiceField
|
||||||
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
@ -88,7 +88,7 @@ class CircuitSerializer(PrimaryModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer):
|
class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
circuit = NestedCircuitSerializer()
|
circuit = NestedCircuitSerializer()
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
@ -99,6 +99,6 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSer
|
|||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||||
'_occupied',
|
'_occupied',
|
||||||
]
|
]
|
||||||
|
21
netbox/circuits/migrations/0004_rename_cable_peer.py
Normal file
21
netbox/circuits/migrations/0004_rename_cable_peer.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0003_extend_tag_support'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from dcim.models import CableTermination, PathEndpoint
|
from dcim.models import LinkTermination, PathEndpoint
|
||||||
from extras.models import ObjectChange
|
from extras.models import ObjectChange
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||||
@ -256,7 +256,7 @@ class Circuit(PrimaryModel):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks')
|
||||||
class CircuitTermination(ChangeLoggedModel, CableTermination):
|
class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
||||||
circuit = models.ForeignKey(
|
circuit = models.ForeignKey(
|
||||||
to='circuits.Circuit',
|
to='circuits.Circuit',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -17,28 +17,29 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
|
|||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedClusterSerializer
|
from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||||
|
from wireless.choices import *
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
class CableTerminationSerializer(serializers.ModelSerializer):
|
class LinkTerminationSerializer(serializers.ModelSerializer):
|
||||||
cable_peer_type = serializers.SerializerMethodField(read_only=True)
|
link_peer_type = serializers.SerializerMethodField(read_only=True)
|
||||||
cable_peer = serializers.SerializerMethodField(read_only=True)
|
link_peer = serializers.SerializerMethodField(read_only=True)
|
||||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def get_cable_peer_type(self, obj):
|
def get_link_peer_type(self, obj):
|
||||||
if obj._cable_peer is not None:
|
if obj._link_peer is not None:
|
||||||
return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}'
|
return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_cable_peer(self, obj):
|
def get_link_peer(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the cable termination model.
|
Return the appropriate serializer for the link termination model.
|
||||||
"""
|
"""
|
||||||
if obj._cable_peer is not None:
|
if obj._link_peer is not None:
|
||||||
serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested')
|
serializer = get_serializer_for_model(obj._link_peer, prefix='Nested')
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj._cable_peer, context=context).data
|
return serializer(obj._link_peer, context=context).data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||||
@ -503,7 +504,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -522,12 +523,12 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerial
|
|||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -546,12 +547,12 @@ class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
|||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -575,12 +576,12 @@ class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
|||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -594,18 +595,20 @@ class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
|||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||||
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -620,10 +623,10 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||||
'wwn', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
|
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||||
'_occupied',
|
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -640,7 +643,7 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
|||||||
return super().validate(data)
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
@ -650,7 +653,7 @@ class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
|||||||
model = RearPort
|
model = RearPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', '_occupied',
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -666,7 +669,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'label']
|
fields = ['id', 'url', 'display', 'name', 'label']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
@ -677,7 +680,7 @@ class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
|||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||||
'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields',
|
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', '_occupied',
|
'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -728,7 +731,7 @@ class CableSerializer(PrimaryModelSerializer):
|
|||||||
)
|
)
|
||||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||||
|
|
||||||
@ -853,7 +856,7 @@ class PowerPanelSerializer(PrimaryModelSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
power_panel = NestedPowerPanelSerializer()
|
power_panel = NestedPowerPanelSerializer()
|
||||||
rack = NestedRackSerializer(
|
rack = NestedRackSerializer(
|
||||||
@ -883,7 +886,7 @@ class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
|||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', '_occupied',
|
'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
@ -513,7 +513,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
filterset_class = filtersets.ConsolePortFilterSet
|
filterset_class = filtersets.ConsolePortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
@ -521,7 +521,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
|
|
||||||
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||||
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
|
'device', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||||
@ -529,14 +529,14 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
filterset_class = filtersets.PowerPortFilterSet
|
filterset_class = filtersets.PowerPortFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
filterset_class = filtersets.PowerOutletFilterSet
|
filterset_class = filtersets.PowerOutletFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
@ -544,7 +544,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
|
|
||||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
'device', 'parent', 'lag', '_path__destination', 'cable', '_link_peer', 'ip_addresses', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
@ -625,7 +625,7 @@ class PowerPanelViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
||||||
queryset = PowerFeed.objects.prefetch_related(
|
queryset = PowerFeed.objects.prefetch_related(
|
||||||
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
|
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerFeedSerializer
|
serializer_class = serializers.PowerFeedSerializer
|
||||||
filterset_class = filtersets.PowerFeedFilterSet
|
filterset_class = filtersets.PowerFeedFilterSet
|
||||||
|
@ -1061,7 +1061,7 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables/links
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableTypeChoices(ChoiceSet):
|
class CableTypeChoices(ChoiceSet):
|
||||||
@ -1125,7 +1125,7 @@ class CableTypeChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CableStatusChoices(ChoiceSet):
|
class LinkStatusChoices(ChoiceSet):
|
||||||
|
|
||||||
STATUS_CONNECTED = 'connected'
|
STATUS_CONNECTED = 'connected'
|
||||||
STATUS_PLANNED = 'planned'
|
STATUS_PLANNED = 'planned'
|
||||||
|
@ -42,6 +42,7 @@ WIRELESS_IFACE_TYPES = [
|
|||||||
InterfaceTypeChoices.TYPE_80211N,
|
InterfaceTypeChoices.TYPE_80211N,
|
||||||
InterfaceTypeChoices.TYPE_80211AC,
|
InterfaceTypeChoices.TYPE_80211AC,
|
||||||
InterfaceTypeChoices.TYPE_80211AD,
|
InterfaceTypeChoices.TYPE_80211AD,
|
||||||
|
InterfaceTypeChoices.TYPE_80211AX,
|
||||||
]
|
]
|
||||||
|
|
||||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||||
|
@ -14,6 +14,7 @@ from utilities.filters import (
|
|||||||
TreeNodeMultipleChoiceFilter,
|
TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import *
|
from .models import *
|
||||||
@ -987,10 +988,19 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
|
rf_role = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessRoleChoices
|
||||||
|
)
|
||||||
|
rf_channel = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessChannelChoices
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
|
fields = [
|
||||||
|
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
|
||||||
|
'rf_channel_frequency', 'rf_channel_width', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
try:
|
try:
|
||||||
@ -1202,7 +1212,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
|||||||
choices=CableTypeChoices
|
choices=CableTypeChoices
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=CableStatusChoices
|
choices=LinkStatusChoices
|
||||||
)
|
)
|
||||||
color = django_filters.MultipleChoiceFilter(
|
color = django_filters.MultipleChoiceFilter(
|
||||||
choices=ColorChoices
|
choices=ColorChoices
|
||||||
|
@ -463,7 +463,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
|||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
choices=add_blank_choice(CableStatusChoices),
|
choices=add_blank_choice(LinkStatusChoices),
|
||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect(),
|
widget=StaticSelect(),
|
||||||
initial=''
|
initial=''
|
||||||
@ -940,7 +940,7 @@ class PowerOutletBulkEditForm(
|
|||||||
class InterfaceBulkEditForm(
|
class InterfaceBulkEditForm(
|
||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
|
'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
|
||||||
'mode',
|
'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||||
]),
|
]),
|
||||||
BootstrapMixin,
|
BootstrapMixin,
|
||||||
AddRemoveTagsForm,
|
AddRemoveTagsForm,
|
||||||
@ -991,8 +991,8 @@ class InterfaceBulkEditForm(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'untagged_vlan',
|
'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
|
||||||
'tagged_vlans',
|
'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -11,6 +11,7 @@ from extras.forms import CustomFieldModelCSVForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
from wireless.choices import WirelessRoleChoices
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableCSVForm',
|
'CableCSVForm',
|
||||||
@ -584,12 +585,18 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||||
)
|
)
|
||||||
|
rf_role = CSVChoiceField(
|
||||||
|
choices=WirelessRoleChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Wireless role (AP/station)'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn',
|
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn',
|
||||||
'mtu', 'mgmt_only', 'description', 'mode',
|
'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
|
'rf_channel_width',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -812,7 +819,7 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
|||||||
|
|
||||||
# Cable attributes
|
# Cable attributes
|
||||||
status = CSVChoiceField(
|
status = CSVChoiceField(
|
||||||
choices=CableStatusChoices,
|
choices=LinkStatusChoices,
|
||||||
required=False,
|
required=False,
|
||||||
help_text='Connection status'
|
help_text='Connection status'
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ from utilities.forms import (
|
|||||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
|
from wireless.choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableFilterForm',
|
'CableFilterForm',
|
||||||
@ -735,7 +736,7 @@ class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterF
|
|||||||
)
|
)
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
choices=add_blank_choice(CableStatusChoices),
|
choices=add_blank_choice(LinkStatusChoices),
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
color = ColorField(
|
color = ColorField(
|
||||||
@ -966,6 +967,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||||
|
['rf_role', 'rf_channel', 'rf_channel_width'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||||
]
|
]
|
||||||
kind = forms.MultipleChoiceField(
|
kind = forms.MultipleChoiceField(
|
||||||
@ -998,6 +1000,26 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='WWN'
|
label='WWN'
|
||||||
)
|
)
|
||||||
|
rf_role = forms.MultipleChoiceField(
|
||||||
|
choices=WirelessRoleChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelectMultiple(),
|
||||||
|
label='Wireless role'
|
||||||
|
)
|
||||||
|
rf_channel = forms.MultipleChoiceField(
|
||||||
|
choices=WirelessChannelChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelectMultiple(),
|
||||||
|
label='Wireless channel'
|
||||||
|
)
|
||||||
|
rf_channel_frequency = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label='Channel frequency (MHz)'
|
||||||
|
)
|
||||||
|
rf_channel_width = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label='Channel width (MHz)'
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ from utilities.forms import (
|
|||||||
SlugField, StaticSelect,
|
SlugField, StaticSelect,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
from .common import InterfaceCommonForm
|
from .common import InterfaceCommonForm
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -1100,6 +1101,19 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
'type': 'lag',
|
'type': 'lag',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
wireless_lan_group = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Wireless LAN group'
|
||||||
|
)
|
||||||
|
wireless_lans = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Wireless LANs',
|
||||||
|
query_params={
|
||||||
|
'group_id': '$wireless_lan_group',
|
||||||
|
}
|
||||||
|
)
|
||||||
vlan_group = DynamicModelChoiceField(
|
vlan_group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1130,18 +1144,23 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||||
'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
|
'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
'mode': StaticSelect(),
|
'mode': StaticSelect(),
|
||||||
|
'rf_role': StaticSelect(),
|
||||||
|
'rf_channel': StaticSelect(),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mode': INTERFACE_MODE_HELP_TEXT,
|
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||||
|
'rf_channel_frequency': "Populated by selected channel (if set)",
|
||||||
|
'rf_channel_width': "Populated by selected channel (if set)",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -10,6 +10,7 @@ from utilities.forms import (
|
|||||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
ExpandableNameField, StaticSelect,
|
ExpandableNameField, StaticSelect,
|
||||||
)
|
)
|
||||||
|
from wireless.choices import *
|
||||||
from .common import InterfaceCommonForm
|
from .common import InterfaceCommonForm
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -465,7 +466,27 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
|||||||
mode = forms.ChoiceField(
|
mode = forms.ChoiceField(
|
||||||
choices=add_blank_choice(InterfaceModeChoices),
|
choices=add_blank_choice(InterfaceModeChoices),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
rf_role = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WirelessRoleChoices),
|
||||||
|
required=False,
|
||||||
widget=StaticSelect(),
|
widget=StaticSelect(),
|
||||||
|
label='Wireless role'
|
||||||
|
)
|
||||||
|
rf_channel = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WirelessChannelChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect(),
|
||||||
|
label='Wireless channel'
|
||||||
|
)
|
||||||
|
rf_channel_frequency = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
label='Channel frequency (MHz)'
|
||||||
|
)
|
||||||
|
rf_channel_width = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
label='Channel width (MHz)'
|
||||||
)
|
)
|
||||||
untagged_vlan = DynamicModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -477,7 +498,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
|||||||
)
|
)
|
||||||
field_order = (
|
field_order = (
|
||||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||||
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
|
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -212,6 +212,12 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType):
|
|||||||
def resolve_mode(self, info):
|
def resolve_mode(self, info):
|
||||||
return self.mode or None
|
return self.mode or None
|
||||||
|
|
||||||
|
def resolve_rf_role(self, info):
|
||||||
|
return self.rf_role or None
|
||||||
|
|
||||||
|
def resolve_rf_channel(self, info):
|
||||||
|
return self.rf_channel or None
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateType(ComponentTemplateObjectType):
|
class InterfaceTemplateType(ComponentTemplateObjectType):
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.core.management.color import no_style
|
from django.core.management.color import no_style
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
||||||
from dcim.signals import create_cablepath
|
from dcim.signals import create_cablepath
|
||||||
@ -67,7 +68,10 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Retrace paths
|
# Retrace paths
|
||||||
for model in ENDPOINT_MODELS:
|
for model in ENDPOINT_MODELS:
|
||||||
origins = model.objects.filter(cable__isnull=False)
|
params = Q(cable__isnull=False)
|
||||||
|
if hasattr(model, 'wireless_link'):
|
||||||
|
params |= Q(wireless_link__isnull=False)
|
||||||
|
origins = model.objects.filter(params)
|
||||||
if not options['force']:
|
if not options['force']:
|
||||||
origins = origins.filter(_path__isnull=True)
|
origins = origins.filter(_path__isnull=True)
|
||||||
origins_count = origins.count()
|
origins_count = origins.count()
|
||||||
|
91
netbox/dcim/migrations/0139_rename_cable_peer.py
Normal file
91
netbox/dcim/migrations/0139_rename_cable_peer.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0138_extend_tag_support'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='consoleport',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='consoleport',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='frontport',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='frontport',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='interface',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='interface',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='powerport',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='powerport',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='rearport',
|
||||||
|
old_name='_cable_peer_id',
|
||||||
|
new_name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='rearport',
|
||||||
|
old_name='_cable_peer_type',
|
||||||
|
new_name='_link_peer_type',
|
||||||
|
),
|
||||||
|
]
|
43
netbox/dcim/migrations/0140_wireless.py
Normal file
43
netbox/dcim/migrations/0140_wireless.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0139_rename_cable_peer'),
|
||||||
|
('wireless', '0001_wireless'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='rf_role',
|
||||||
|
field=models.CharField(blank=True, max_length=30),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='rf_channel',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='rf_channel_frequency',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='rf_channel_width',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='wireless_lans',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='wireless_link',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'),
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,7 @@ __all__ = (
|
|||||||
'BaseInterface',
|
'BaseInterface',
|
||||||
'Cable',
|
'Cable',
|
||||||
'CablePath',
|
'CablePath',
|
||||||
'CableTermination',
|
'LinkTermination',
|
||||||
'ConsolePort',
|
'ConsolePort',
|
||||||
'ConsolePortTemplate',
|
'ConsolePortTemplate',
|
||||||
'ConsoleServerPort',
|
'ConsoleServerPort',
|
||||||
|
@ -64,8 +64,8 @@ class Cable(PrimaryModel):
|
|||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CableStatusChoices,
|
choices=LinkStatusChoices,
|
||||||
default=CableStatusChoices.STATUS_CONNECTED
|
default=LinkStatusChoices.STATUS_CONNECTED
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
@ -292,7 +292,7 @@ class Cable(PrimaryModel):
|
|||||||
self._pk = self.pk
|
self._pk = self.pk
|
||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return CableStatusChoices.CSS_CLASSES.get(self.status)
|
return LinkStatusChoices.CSS_CLASSES.get(self.status)
|
||||||
|
|
||||||
def get_compatible_types(self):
|
def get_compatible_types(self):
|
||||||
"""
|
"""
|
||||||
@ -386,7 +386,7 @@ class CablePath(BigIDModel):
|
|||||||
"""
|
"""
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
if origin is None or origin.cable is None:
|
if origin is None or origin.link is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
destination = None
|
destination = None
|
||||||
@ -396,13 +396,13 @@ class CablePath(BigIDModel):
|
|||||||
is_split = False
|
is_split = False
|
||||||
|
|
||||||
node = origin
|
node = origin
|
||||||
while node.cable is not None:
|
while node.link is not None:
|
||||||
if node.cable.status != CableStatusChoices.STATUS_CONNECTED:
|
if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED:
|
||||||
is_active = False
|
is_active = False
|
||||||
|
|
||||||
# Follow the cable to its far-end termination
|
# Follow the link to its far-end termination
|
||||||
path.append(object_to_path_node(node.cable))
|
path.append(object_to_path_node(node.link))
|
||||||
peer_termination = node.get_cable_peer()
|
peer_termination = node.get_link_peer()
|
||||||
|
|
||||||
# Follow a FrontPort to its corresponding RearPort
|
# Follow a FrontPort to its corresponding RearPort
|
||||||
if isinstance(peer_termination, FrontPort):
|
if isinstance(peer_termination, FrontPort):
|
||||||
|
@ -18,11 +18,13 @@ from utilities.mptt import TreeManager
|
|||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.utils import get_channel_attr
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseInterface',
|
'BaseInterface',
|
||||||
'CableTermination',
|
'LinkTermination',
|
||||||
'ConsolePort',
|
'ConsolePort',
|
||||||
'ConsoleServerPort',
|
'ConsoleServerPort',
|
||||||
'DeviceBay',
|
'DeviceBay',
|
||||||
@ -87,14 +89,14 @@ class ComponentModel(PrimaryModel):
|
|||||||
return self.device
|
return self.device
|
||||||
|
|
||||||
|
|
||||||
class CableTermination(models.Model):
|
class LinkTermination(models.Model):
|
||||||
"""
|
"""
|
||||||
An abstract model inherited by all models to which a Cable can terminate (certain device components, PowerFeed, and
|
An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples
|
||||||
CircuitTermination instances). The `cable` field indicates the Cable instance which is terminated to this instance.
|
include most device components, CircuitTerminations, and PowerFeeds. The `cable` and `wireless_link` fields
|
||||||
|
reference the attached Cable or WirelessLink instance, respectively.
|
||||||
|
|
||||||
`_cable_peer` is a GenericForeignKey used to cache the far-end CableTermination on the local instance; this is a
|
`_link_peer` is a GenericForeignKey used to cache the far-end LinkTermination on the local instance; this is a
|
||||||
shortcut to referencing `cable.termination_b`, for example. `_cable_peer` is set or cleared by the receivers in
|
shortcut to referencing `instance.link.termination_b`, for example.
|
||||||
dcim.signals when a Cable instance is created or deleted, respectively.
|
|
||||||
"""
|
"""
|
||||||
cable = models.ForeignKey(
|
cable = models.ForeignKey(
|
||||||
to='dcim.Cable',
|
to='dcim.Cable',
|
||||||
@ -103,20 +105,20 @@ class CableTermination(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_cable_peer_type = models.ForeignKey(
|
_link_peer_type = models.ForeignKey(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_cable_peer_id = models.PositiveIntegerField(
|
_link_peer_id = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_cable_peer = GenericForeignKey(
|
_link_peer = GenericForeignKey(
|
||||||
ct_field='_cable_peer_type',
|
ct_field='_link_peer_type',
|
||||||
fk_field='_cable_peer_id'
|
fk_field='_link_peer_id'
|
||||||
)
|
)
|
||||||
mark_connected = models.BooleanField(
|
mark_connected = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@ -146,8 +148,8 @@ class CableTermination(models.Model):
|
|||||||
"mark_connected": "Cannot mark as connected with a cable attached."
|
"mark_connected": "Cannot mark as connected with a cable attached."
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_cable_peer(self):
|
def get_link_peer(self):
|
||||||
return self._cable_peer
|
return self._link_peer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _occupied(self):
|
def _occupied(self):
|
||||||
@ -157,6 +159,13 @@ class CableTermination(models.Model):
|
|||||||
def parent_object(self):
|
def parent_object(self):
|
||||||
raise NotImplementedError("CableTermination models must implement parent_object()")
|
raise NotImplementedError("CableTermination models must implement parent_object()")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def link(self):
|
||||||
|
"""
|
||||||
|
Generic wrapper for a Cable, WirelessLink, or some other relation to a connected termination.
|
||||||
|
"""
|
||||||
|
return self.cable
|
||||||
|
|
||||||
|
|
||||||
class PathEndpoint(models.Model):
|
class PathEndpoint(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -219,7 +228,7 @@ class PathEndpoint(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -251,7 +260,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
@ -283,7 +292,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -333,8 +342,8 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet)
|
poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet)
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
||||||
utilization = PowerPort.objects.filter(
|
utilization = PowerPort.objects.filter(
|
||||||
_cable_peer_type=poweroutlet_ct,
|
_link_peer_type=poweroutlet_ct,
|
||||||
_cable_peer_id__in=outlet_ids
|
_link_peer_id__in=outlet_ids
|
||||||
).aggregate(
|
).aggregate(
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
maximum_draw_total=Sum('maximum_draw'),
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
allocated_draw_total=Sum('allocated_draw'),
|
||||||
@ -347,12 +356,12 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Calculate per-leg aggregates for three-phase feeds
|
# Calculate per-leg aggregates for three-phase feeds
|
||||||
if getattr(self._cable_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE:
|
if getattr(self._link_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE:
|
||||||
for leg, leg_name in PowerOutletFeedLegChoices:
|
for leg, leg_name in PowerOutletFeedLegChoices:
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
||||||
utilization = PowerPort.objects.filter(
|
utilization = PowerPort.objects.filter(
|
||||||
_cable_peer_type=poweroutlet_ct,
|
_link_peer_type=poweroutlet_ct,
|
||||||
_cable_peer_id__in=outlet_ids
|
_link_peer_id__in=outlet_ids
|
||||||
).aggregate(
|
).aggregate(
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
maximum_draw_total=Sum('maximum_draw'),
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
allocated_draw_total=Sum('allocated_draw'),
|
||||||
@ -380,7 +389,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
class PowerOutlet(ComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
@ -475,7 +484,7 @@ class BaseInterface(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
"""
|
"""
|
||||||
@ -517,6 +526,45 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
verbose_name='WWN',
|
verbose_name='WWN',
|
||||||
help_text='64-bit World Wide Name'
|
help_text='64-bit World Wide Name'
|
||||||
)
|
)
|
||||||
|
rf_role = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=WirelessRoleChoices,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Wireless role'
|
||||||
|
)
|
||||||
|
rf_channel = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WirelessChannelChoices,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Wireless channel'
|
||||||
|
)
|
||||||
|
rf_channel_frequency = models.DecimalField(
|
||||||
|
max_digits=7,
|
||||||
|
decimal_places=2,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Channel frequency (MHz)'
|
||||||
|
)
|
||||||
|
rf_channel_width = models.DecimalField(
|
||||||
|
max_digits=7,
|
||||||
|
decimal_places=3,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Channel width (MHz)'
|
||||||
|
)
|
||||||
|
wireless_link = models.ForeignKey(
|
||||||
|
to='wireless.WirelessLink',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
wireless_lans = models.ManyToManyField(
|
||||||
|
to='wireless.WirelessLAN',
|
||||||
|
related_name='interfaces',
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Wireless LANs'
|
||||||
|
)
|
||||||
untagged_vlan = models.ForeignKey(
|
untagged_vlan = models.ForeignKey(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -550,14 +598,14 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Virtual interfaces cannot be connected
|
# Virtual Interfaces cannot have a Cable attached
|
||||||
if not self.is_connectable and self.cable:
|
if self.is_virtual and self.cable:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
|
'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
|
||||||
})
|
})
|
||||||
|
|
||||||
# Non-connectable interfaces cannot be marked as connected
|
# Virtual Interfaces cannot be marked as connected
|
||||||
if not self.is_connectable and self.mark_connected:
|
if self.is_virtual and self.mark_connected:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
|
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
|
||||||
})
|
})
|
||||||
@ -603,6 +651,34 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
if self.pk and self.lag_id == self.pk:
|
if self.pk and self.lag_id == self.pk:
|
||||||
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||||
|
|
||||||
|
# RF role & channel may only be set for wireless interfaces
|
||||||
|
if self.rf_role and not self.is_wireless:
|
||||||
|
raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."})
|
||||||
|
if self.rf_channel and not self.is_wireless:
|
||||||
|
raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."})
|
||||||
|
|
||||||
|
# Validate channel frequency against interface type and selected channel (if any)
|
||||||
|
if self.rf_channel_frequency:
|
||||||
|
if not self.is_wireless:
|
||||||
|
raise ValidationError({
|
||||||
|
'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.",
|
||||||
|
})
|
||||||
|
if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
|
||||||
|
raise ValidationError({
|
||||||
|
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
|
||||||
|
})
|
||||||
|
elif self.rf_channel:
|
||||||
|
self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')
|
||||||
|
|
||||||
|
# Validate channel width against interface type and selected channel (if any)
|
||||||
|
if self.rf_channel_width:
|
||||||
|
if not self.is_wireless:
|
||||||
|
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
|
||||||
|
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
|
||||||
|
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
|
||||||
|
elif self.rf_channel:
|
||||||
|
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
|
||||||
|
|
||||||
# Validate untagged VLAN
|
# Validate untagged VLAN
|
||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -611,8 +687,12 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connectable(self):
|
def _occupied(self):
|
||||||
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
return super()._occupied or bool(self.wireless_link_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wired(self):
|
||||||
|
return not self.is_virtual and not self.is_wireless
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_virtual(self):
|
def is_virtual(self):
|
||||||
@ -626,13 +706,17 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
def is_lag(self):
|
def is_lag(self):
|
||||||
return self.type == InterfaceTypeChoices.TYPE_LAG
|
return self.type == InterfaceTypeChoices.TYPE_LAG
|
||||||
|
|
||||||
|
@property
|
||||||
|
def link(self):
|
||||||
|
return self.cable or self.wireless_link
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class FrontPort(ComponentModel, CableTermination):
|
class FrontPort(ComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
"""
|
"""
|
||||||
@ -686,7 +770,7 @@ class FrontPort(ComponentModel, CableTermination):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RearPort(ComponentModel, CableTermination):
|
class RearPort(ComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
"""
|
"""
|
||||||
|
@ -10,7 +10,7 @@ from extras.utils import extras_features
|
|||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.validators import ExclusionValidator
|
from utilities.validators import ExclusionValidator
|
||||||
from .device_components import CableTermination, PathEndpoint
|
from .device_components import LinkTermination, PathEndpoint
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PowerFeed',
|
'PowerFeed',
|
||||||
@ -72,7 +72,7 @@ class PowerPanel(PrimaryModel):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
|
||||||
"""
|
"""
|
||||||
An electrical circuit delivered from a PowerPanel.
|
An electrical circuit delivered from a PowerPanel.
|
||||||
"""
|
"""
|
||||||
|
@ -427,13 +427,13 @@ class Rack(PrimaryModel):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
pf_powerports = PowerPort.objects.filter(
|
pf_powerports = PowerPort.objects.filter(
|
||||||
_cable_peer_type=ContentType.objects.get_for_model(PowerFeed),
|
_link_peer_type=ContentType.objects.get_for_model(PowerFeed),
|
||||||
_cable_peer_id__in=powerfeeds.values_list('id', flat=True)
|
_link_peer_id__in=powerfeeds.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports)
|
poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports)
|
||||||
allocated_draw_total = PowerPort.objects.filter(
|
allocated_draw_total = PowerPort.objects.filter(
|
||||||
_cable_peer_type=ContentType.objects.get_for_model(PowerOutlet),
|
_link_peer_type=ContentType.objects.get_for_model(PowerOutlet),
|
||||||
_cable_peer_id__in=poweroutlets.values_list('id', flat=True)
|
_link_peer_id__in=poweroutlets.values_list('id', flat=True)
|
||||||
).aggregate(Sum('allocated_draw'))['allocated_draw__sum'] or 0
|
).aggregate(Sum('allocated_draw'))['allocated_draw__sum'] or 0
|
||||||
|
|
||||||
return int(allocated_draw_total / available_power_total * 100)
|
return int(allocated_draw_total / available_power_total * 100)
|
||||||
|
@ -2,37 +2,11 @@ import logging
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||||
from django.db import transaction
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .choices import CableStatusChoices
|
from .choices import LinkStatusChoices
|
||||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
||||||
|
from .utils import create_cablepath, rebuild_paths
|
||||||
|
|
||||||
def create_cablepath(node):
|
|
||||||
"""
|
|
||||||
Create CablePaths for all paths originating from the specified node.
|
|
||||||
"""
|
|
||||||
cp = CablePath.from_origin(node)
|
|
||||||
if cp:
|
|
||||||
try:
|
|
||||||
cp.save()
|
|
||||||
except Exception as e:
|
|
||||||
print(node, node.pk)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def rebuild_paths(obj):
|
|
||||||
"""
|
|
||||||
Rebuild all CablePaths which traverse the specified node
|
|
||||||
"""
|
|
||||||
cable_paths = CablePath.objects.filter(path__contains=obj)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
for cp in cable_paths:
|
|
||||||
cp.delete()
|
|
||||||
if cp.origin:
|
|
||||||
create_cablepath(cp.origin)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -109,12 +83,12 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
|||||||
if instance.termination_a.cable != instance:
|
if instance.termination_a.cable != instance:
|
||||||
logger.debug(f"Updating termination A for cable {instance}")
|
logger.debug(f"Updating termination A for cable {instance}")
|
||||||
instance.termination_a.cable = instance
|
instance.termination_a.cable = instance
|
||||||
instance.termination_a._cable_peer = instance.termination_b
|
instance.termination_a._link_peer = instance.termination_b
|
||||||
instance.termination_a.save()
|
instance.termination_a.save()
|
||||||
if instance.termination_b.cable != instance:
|
if instance.termination_b.cable != instance:
|
||||||
logger.debug(f"Updating termination B for cable {instance}")
|
logger.debug(f"Updating termination B for cable {instance}")
|
||||||
instance.termination_b.cable = instance
|
instance.termination_b.cable = instance
|
||||||
instance.termination_b._cable_peer = instance.termination_a
|
instance.termination_b._link_peer = instance.termination_a
|
||||||
instance.termination_b.save()
|
instance.termination_b.save()
|
||||||
|
|
||||||
# Create/update cable paths
|
# Create/update cable paths
|
||||||
@ -128,7 +102,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
|||||||
# We currently don't support modifying either termination of an existing Cable. (This
|
# We currently don't support modifying either termination of an existing Cable. (This
|
||||||
# may change in the future.) However, we do need to capture status changes and update
|
# may change in the future.) However, we do need to capture status changes and update
|
||||||
# any CablePaths accordingly.
|
# any CablePaths accordingly.
|
||||||
if instance.status != CableStatusChoices.STATUS_CONNECTED:
|
if instance.status != LinkStatusChoices.STATUS_CONNECTED:
|
||||||
CablePath.objects.filter(path__contains=instance).update(is_active=False)
|
CablePath.objects.filter(path__contains=instance).update(is_active=False)
|
||||||
else:
|
else:
|
||||||
rebuild_paths(instance)
|
rebuild_paths(instance)
|
||||||
@ -145,11 +119,11 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
if instance.termination_a is not None:
|
if instance.termination_a is not None:
|
||||||
logger.debug(f"Nullifying termination A for cable {instance}")
|
logger.debug(f"Nullifying termination A for cable {instance}")
|
||||||
model = instance.termination_a._meta.model
|
model = instance.termination_a._meta.model
|
||||||
model.objects.filter(pk=instance.termination_a.pk).update(_cable_peer_type=None, _cable_peer_id=None)
|
model.objects.filter(pk=instance.termination_a.pk).update(_link_peer_type=None, _link_peer_id=None)
|
||||||
if instance.termination_b is not None:
|
if instance.termination_b is not None:
|
||||||
logger.debug(f"Nullifying termination B for cable {instance}")
|
logger.debug(f"Nullifying termination B for cable {instance}")
|
||||||
model = instance.termination_b._meta.model
|
model = instance.termination_b._meta.model
|
||||||
model.objects.filter(pk=instance.termination_b.pk).update(_cable_peer_type=None, _cable_peer_id=None)
|
model.objects.filter(pk=instance.termination_b.pk).update(_link_peer_type=None, _link_peer_id=None)
|
||||||
|
|
||||||
# Delete and retrace any dependent cable paths
|
# Delete and retrace any dependent cable paths
|
||||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
for cablepath in CablePath.objects.filter(path__contains=instance):
|
||||||
|
@ -398,6 +398,39 @@ class CableTraceSVG:
|
|||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
def _draw_wirelesslink(self, url, labels):
|
||||||
|
"""
|
||||||
|
Draw a line with labels representing a WirelessLink.
|
||||||
|
|
||||||
|
:param url: Hyperlink URL
|
||||||
|
:param labels: Iterable of text labels
|
||||||
|
"""
|
||||||
|
group = Group(class_='connector')
|
||||||
|
|
||||||
|
# Draw the wireless link
|
||||||
|
start = (OFFSET + self.center, self.cursor)
|
||||||
|
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||||
|
end = (start[0], start[1] + height)
|
||||||
|
line = Line(start=start, end=end, class_='wireless-link')
|
||||||
|
group.add(line)
|
||||||
|
|
||||||
|
self.cursor += PADDING * 2
|
||||||
|
|
||||||
|
# Add link
|
||||||
|
link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
|
||||||
|
|
||||||
|
# Add text label(s)
|
||||||
|
for i, label in enumerate(labels):
|
||||||
|
self.cursor += LINE_HEIGHT
|
||||||
|
text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
|
||||||
|
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||||
|
link.add(text)
|
||||||
|
|
||||||
|
group.add(link)
|
||||||
|
self.cursor += PADDING * 2
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
def _draw_attachment(self):
|
def _draw_attachment(self):
|
||||||
"""
|
"""
|
||||||
Return an SVG group containing a line element and "Attachment" label.
|
Return an SVG group containing a line element and "Attachment" label.
|
||||||
@ -418,6 +451,9 @@ class CableTraceSVG:
|
|||||||
"""
|
"""
|
||||||
Return an SVG document representing a cable trace.
|
Return an SVG document representing a cable trace.
|
||||||
"""
|
"""
|
||||||
|
from dcim.models import Cable
|
||||||
|
from wireless.models import WirelessLink
|
||||||
|
|
||||||
traced_path = self.origin.trace()
|
traced_path = self.origin.trace()
|
||||||
|
|
||||||
# Prep elements list
|
# Prep elements list
|
||||||
@ -452,24 +488,39 @@ class CableTraceSVG:
|
|||||||
)
|
)
|
||||||
terminations.append(termination)
|
terminations.append(termination)
|
||||||
|
|
||||||
# Connector (either a Cable or attachment to a ProviderNetwork)
|
# Connector (a Cable or WirelessLink)
|
||||||
if connector is not None:
|
if connector is not None:
|
||||||
|
|
||||||
# Cable
|
# Cable
|
||||||
cable_labels = [
|
if type(connector) is Cable:
|
||||||
f'Cable {connector}',
|
connector_labels = [
|
||||||
connector.get_status_display()
|
f'Cable {connector}',
|
||||||
]
|
connector.get_status_display()
|
||||||
if connector.type:
|
]
|
||||||
cable_labels.append(connector.get_type_display())
|
if connector.type:
|
||||||
if connector.length and connector.length_unit:
|
connector_labels.append(connector.get_type_display())
|
||||||
cable_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
|
if connector.length and connector.length_unit:
|
||||||
cable = self._draw_cable(
|
connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
|
||||||
color=connector.color or '000000',
|
cable = self._draw_cable(
|
||||||
url=connector.get_absolute_url(),
|
color=connector.color or '000000',
|
||||||
labels=cable_labels
|
url=connector.get_absolute_url(),
|
||||||
)
|
labels=connector_labels
|
||||||
connectors.append(cable)
|
)
|
||||||
|
connectors.append(cable)
|
||||||
|
|
||||||
|
# WirelessLink
|
||||||
|
elif type(connector) is WirelessLink:
|
||||||
|
connector_labels = [
|
||||||
|
f'Wireless link {connector}',
|
||||||
|
connector.get_status_display()
|
||||||
|
]
|
||||||
|
if connector.ssid:
|
||||||
|
connector_labels.append(connector.ssid)
|
||||||
|
wirelesslink = self._draw_wirelesslink(
|
||||||
|
url=connector.get_absolute_url(),
|
||||||
|
labels=connector_labels
|
||||||
|
)
|
||||||
|
connectors.append(wirelesslink)
|
||||||
|
|
||||||
# Far end termination
|
# Far end termination
|
||||||
termination = self._draw_box(
|
termination = self._draw_box(
|
||||||
|
@ -11,11 +11,7 @@ from utilities.tables import (
|
|||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||||
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
||||||
)
|
)
|
||||||
from .template_code import (
|
from .template_code import *
|
||||||
CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS,
|
|
||||||
FRONTPORT_BUTTONS, INTERFACE_BUTTONS, INTERFACE_IPADDRESSES, INTERFACE_TAGGED_VLANS, POWEROUTLET_BUTTONS,
|
|
||||||
POWERPORT_BUTTONS, REARPORT_BUTTONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseInterfaceTable',
|
'BaseInterfaceTable',
|
||||||
@ -266,11 +262,11 @@ class CableTerminationTable(BaseTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Cable Color'
|
verbose_name='Cable Color'
|
||||||
)
|
)
|
||||||
cable_peer = TemplateColumn(
|
link_peer = TemplateColumn(
|
||||||
accessor='_cable_peer',
|
accessor='_link_peer',
|
||||||
template_code=CABLETERMINATION,
|
template_code=LINKTERMINATION,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Cable Peer'
|
verbose_name='Link Peer'
|
||||||
)
|
)
|
||||||
mark_connected = BooleanColumn()
|
mark_connected = BooleanColumn()
|
||||||
|
|
||||||
@ -278,7 +274,7 @@ class CableTerminationTable(BaseTable):
|
|||||||
class PathEndpointTable(CableTerminationTable):
|
class PathEndpointTable(CableTerminationTable):
|
||||||
connection = TemplateColumn(
|
connection = TemplateColumn(
|
||||||
accessor='_path.last_node',
|
accessor='_path.last_node',
|
||||||
template_code=CABLETERMINATION,
|
template_code=LINKTERMINATION,
|
||||||
verbose_name='Connection',
|
verbose_name='Connection',
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
@ -299,7 +295,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'cable_peer', 'connection', 'tags',
|
'link_peer', 'connection', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||||
|
|
||||||
@ -320,7 +316,7 @@ class DeviceConsolePortTable(ConsolePortTable):
|
|||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'cable_peer', 'connection', 'tags', 'actions'
|
'link_peer', 'connection', 'tags', 'actions'
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
@ -343,7 +339,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'cable_peer', 'connection', 'tags',
|
'link_peer', 'connection', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||||
|
|
||||||
@ -365,7 +361,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
|||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'cable_peer', 'connection', 'tags', 'actions',
|
'link_peer', 'connection', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
@ -388,7 +384,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||||
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||||
|
|
||||||
@ -410,7 +406,7 @@ class DevicePowerPortTable(PowerPortTable):
|
|||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
||||||
@ -439,7 +435,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||||
'cable_color', 'cable_peer', 'connection', 'tags',
|
'cable_color', 'link_peer', 'connection', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||||
|
|
||||||
@ -460,7 +456,7 @@ class DevicePowerOutletTable(PowerOutletTable):
|
|||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
|
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
|
||||||
@ -493,6 +489,14 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
mgmt_only = BooleanColumn()
|
mgmt_only = BooleanColumn()
|
||||||
|
wireless_link = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
wireless_lans = TemplateColumn(
|
||||||
|
template_code=INTERFACE_WIRELESS_LANS,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Wireless LANs'
|
||||||
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='dcim:interface_list'
|
url_name='dcim:interface_list'
|
||||||
)
|
)
|
||||||
@ -501,7 +505,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
||||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mark_connected',
|
||||||
|
'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses',
|
||||||
'untagged_vlan', 'tagged_vlans',
|
'untagged_vlan', 'tagged_vlans',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
@ -509,8 +514,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
|||||||
|
|
||||||
class DeviceInterfaceTable(InterfaceTable):
|
class DeviceInterfaceTable(InterfaceTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
|
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}drag-horizontal-variant'
|
||||||
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
|
'{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet'
|
||||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
@ -533,8 +538,9 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
||||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
|
||||||
|
'tagged_vlans', 'actions',
|
||||||
)
|
)
|
||||||
order_by = ('name',)
|
order_by = ('name',)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
@ -570,7 +576,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
|||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||||
'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
|
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||||
@ -594,10 +600,10 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'cable_peer', 'tags', 'actions',
|
'cable_color', 'link_peer', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
|
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
|
||||||
'actions',
|
'actions',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
@ -621,7 +627,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
|||||||
model = RearPort
|
model = RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'cable_peer', 'tags',
|
'cable_color', 'link_peer', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||||
|
|
||||||
@ -643,10 +649,10 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
model = RearPort
|
model = RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||||
'cable_peer', 'tags', 'actions',
|
'link_peer', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',
|
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
|
@ -71,10 +71,10 @@ class PowerFeedTable(CableTerminationTable):
|
|||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power',
|
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
|
||||||
'comments', 'tags',
|
'comments', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||||
'cable_peer',
|
'link_peer',
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
CABLETERMINATION = """
|
LINKTERMINATION = """
|
||||||
{% if value %}
|
{% if value %}
|
||||||
{% if value.parent_object %}
|
{% if value.parent_object %}
|
||||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||||
@ -64,6 +64,12 @@ INTERFACE_TAGGED_VLANS = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
INTERFACE_WIRELESS_LANS = """
|
||||||
|
{% for wlan in record.wireless_lans.all %}
|
||||||
|
<a href="{{ wlan.get_absolute_url }}">{{ wlan }}</a><br />
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
POWERFEED_CABLE = """
|
POWERFEED_CABLE = """
|
||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace">
|
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace">
|
||||||
@ -195,15 +201,23 @@ INTERFACE_BUTTONS = """
|
|||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if record.cable %}
|
{% if record.link %}
|
||||||
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
{% if record.cable %}
|
||||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||||
{% if perms.dcim.delete_cable %}
|
{% if perms.dcim.delete_cable %}
|
||||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif record.is_connectable and perms.dcim.add_cable %}
|
{% elif record.wireless_link %}
|
||||||
|
{% if perms.wireless.delete_wirelesslink %}
|
||||||
|
<a href="{% url 'wireless:wirelesslink_delete' pk=record.wireless_link.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Delete wireless link" class="btn btn-danger btn-sm">
|
||||||
|
<i class="mdi mdi-wifi-off" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% elif record.is_wired and perms.dcim.add_cable %}
|
||||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||||
{% if not record.mark_connected %}
|
{% if not record.mark_connected %}
|
||||||
@ -221,6 +235,10 @@ INTERFACE_BUTTONS = """
|
|||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif record.is_wireless and perms.wireless.add_wirelesslink %}
|
||||||
|
<a href="{% url 'wireless:wirelesslink_add' %}?site_a={{ record.device.site.pk }}&location_a={{ record.device.location.pk }}&device_a={{ record.device.pk }}&interface_a={{ record.pk }}&site_b={{ record.device.site.pk }}&location_b={{ record.device.location.pk }}" class="btn btn-success btn-sm">
|
||||||
|
<span class="mdi mdi-wifi-plus" aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.choices import CableStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from dcim.utils import object_to_path_node
|
from dcim.utils import object_to_path_node
|
||||||
|
|
||||||
@ -1142,7 +1142,7 @@ class CablePathTestCase(TestCase):
|
|||||||
self.assertEqual(CablePath.objects.count(), 2)
|
self.assertEqual(CablePath.objects.count(), 2)
|
||||||
|
|
||||||
# Change cable 2's status to "planned"
|
# Change cable 2's status to "planned"
|
||||||
cable2.status = CableStatusChoices.STATUS_PLANNED
|
cable2.status = LinkStatusChoices.STATUS_PLANNED
|
||||||
cable2.save()
|
cable2.save()
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
origin=interface1,
|
origin=interface1,
|
||||||
@ -1160,7 +1160,7 @@ class CablePathTestCase(TestCase):
|
|||||||
|
|
||||||
# Change cable 2's status to "connected"
|
# Change cable 2's status to "connected"
|
||||||
cable2 = Cable.objects.get(pk=cable2.pk)
|
cable2 = Cable.objects.get(pk=cable2.pk)
|
||||||
cable2.status = CableStatusChoices.STATUS_CONNECTED
|
cable2.status = LinkStatusChoices.STATUS_CONNECTED
|
||||||
cable2.save()
|
cable2.save()
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
origin=interface1,
|
origin=interface1,
|
||||||
|
@ -9,6 +9,7 @@ from tenancy.models import Tenant, TenantGroup
|
|||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests
|
from utilities.testing import ChangeLoggedFilterSetTests
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
|
|
||||||
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
@ -2063,6 +2064,8 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||||
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||||
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
|
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
|
||||||
|
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)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
@ -2083,11 +2086,11 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'connected': True}
|
params = {'connected': True}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'connected': False}
|
params = {'connected': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_enabled(self):
|
def test_enabled(self):
|
||||||
params = {'enabled': 'true'}
|
params = {'enabled': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
params = {'enabled': 'false'}
|
params = {'enabled': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
@ -2099,7 +2102,7 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'mgmt_only': 'true'}
|
params = {'mgmt_only': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'mgmt_only': 'false'}
|
params = {'mgmt_only': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_mode(self):
|
def test_mode(self):
|
||||||
params = {'mode': InterfaceModeChoices.MODE_ACCESS}
|
params = {'mode': InterfaceModeChoices.MODE_ACCESS}
|
||||||
@ -2176,7 +2179,7 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'cabled': 'true'}
|
params = {'cabled': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'cabled': 'false'}
|
params = {'cabled': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_kind(self):
|
def test_kind(self):
|
||||||
params = {'kind': 'physical'}
|
params = {'kind': 'physical'}
|
||||||
@ -2192,6 +2195,22 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
|
params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rf_role(self):
|
||||||
|
params = {'rf_role': [WirelessRoleChoices.ROLE_AP, WirelessRoleChoices.ROLE_STATION]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rf_channel(self):
|
||||||
|
params = {'rf_channel': [WirelessChannelChoices.CHANNEL_24G_1, WirelessChannelChoices.CHANNEL_5G_32]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rf_channel_frequency(self):
|
||||||
|
params = {'rf_channel_frequency': [2412, 5160]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rf_channel_width(self):
|
||||||
|
params = {'rf_channel_width': [22, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
@ -2864,12 +2883,12 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
||||||
|
|
||||||
def test_label(self):
|
def test_label(self):
|
||||||
@ -2889,9 +2908,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
params = {'status': [CableStatusChoices.STATUS_CONNECTED]}
|
params = {'status': [LinkStatusChoices.STATUS_CONNECTED]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'status': [CableStatusChoices.STATUS_PLANNED]}
|
params = {'status': [LinkStatusChoices.STATUS_PLANNED]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_color(self):
|
def test_color(self):
|
||||||
|
@ -494,9 +494,9 @@ class CableTestCase(TestCase):
|
|||||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||||
self.assertEqual(self.cable.termination_a, interface1)
|
self.assertEqual(self.cable.termination_a, interface1)
|
||||||
self.assertEqual(interface1._cable_peer, interface2)
|
self.assertEqual(interface1._link_peer, interface2)
|
||||||
self.assertEqual(self.cable.termination_b, interface2)
|
self.assertEqual(self.cable.termination_b, interface2)
|
||||||
self.assertEqual(interface2._cable_peer, interface1)
|
self.assertEqual(interface2._link_peer, interface1)
|
||||||
|
|
||||||
def test_cable_deletion(self):
|
def test_cable_deletion(self):
|
||||||
"""
|
"""
|
||||||
@ -508,10 +508,10 @@ class CableTestCase(TestCase):
|
|||||||
self.assertNotEqual(str(self.cable), '#None')
|
self.assertNotEqual(str(self.cable), '#None')
|
||||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
self.assertIsNone(interface1.cable)
|
self.assertIsNone(interface1.cable)
|
||||||
self.assertIsNone(interface1._cable_peer)
|
self.assertIsNone(interface1._link_peer)
|
||||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||||
self.assertIsNone(interface2.cable)
|
self.assertIsNone(interface2.cable)
|
||||||
self.assertIsNone(interface2._cable_peer)
|
self.assertIsNone(interface2._link_peer)
|
||||||
|
|
||||||
def test_cabletermination_deletion(self):
|
def test_cabletermination_deletion(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1944,7 +1944,7 @@ class CableTestCase(
|
|||||||
'termination_b_type': interface_ct.pk,
|
'termination_b_type': interface_ct.pk,
|
||||||
'termination_b_id': interfaces[3].pk,
|
'termination_b_id': interfaces[3].pk,
|
||||||
'type': CableTypeChoices.TYPE_CAT6,
|
'type': CableTypeChoices.TYPE_CAT6,
|
||||||
'status': CableStatusChoices.STATUS_PLANNED,
|
'status': LinkStatusChoices.STATUS_PLANNED,
|
||||||
'label': 'Label',
|
'label': 'Label',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'length': 100,
|
'length': 100,
|
||||||
@ -1961,7 +1961,7 @@ class CableTestCase(
|
|||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'type': CableTypeChoices.TYPE_CAT5E,
|
'type': CableTypeChoices.TYPE_CAT5E,
|
||||||
'status': CableStatusChoices.STATUS_CONNECTED,
|
'status': LinkStatusChoices.STATUS_CONNECTED,
|
||||||
'label': 'New label',
|
'label': 'New label',
|
||||||
'color': '00ff00',
|
'color': '00ff00',
|
||||||
'length': 50,
|
'length': 50,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
def compile_path_node(ct_id, object_id):
|
def compile_path_node(ct_id, object_id):
|
||||||
@ -26,3 +27,29 @@ def path_node_to_object(repr):
|
|||||||
ct_id, object_id = decompile_path_node(repr)
|
ct_id, object_id = decompile_path_node(repr)
|
||||||
ct = ContentType.objects.get_for_id(ct_id)
|
ct = ContentType.objects.get_for_id(ct_id)
|
||||||
return ct.model_class().objects.get(pk=object_id)
|
return ct.model_class().objects.get(pk=object_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_cablepath(node):
|
||||||
|
"""
|
||||||
|
Create CablePaths for all paths originating from the specified node.
|
||||||
|
"""
|
||||||
|
from dcim.models import CablePath
|
||||||
|
|
||||||
|
cp = CablePath.from_origin(node)
|
||||||
|
if cp:
|
||||||
|
cp.save()
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_paths(obj):
|
||||||
|
"""
|
||||||
|
Rebuild all CablePaths which traverse the specified node
|
||||||
|
"""
|
||||||
|
from dcim.models import CablePath
|
||||||
|
|
||||||
|
cable_paths = CablePath.objects.filter(path__contains=obj)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for cp in cable_paths:
|
||||||
|
cp.delete()
|
||||||
|
if cp.origin:
|
||||||
|
create_cablepath(cp.origin)
|
||||||
|
@ -308,6 +308,7 @@ class APIRootView(APIView):
|
|||||||
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
||||||
('users', reverse('users-api:api-root', request=request, format=format)),
|
('users', reverse('users-api:api-root', request=request, format=format)),
|
||||||
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
|
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
|
||||||
|
('wireless', reverse('wireless-api:api-root', request=request, format=format)),
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from ipam.graphql.schema import IPAMQuery
|
|||||||
from tenancy.graphql.schema import TenancyQuery
|
from tenancy.graphql.schema import TenancyQuery
|
||||||
from users.graphql.schema import UsersQuery
|
from users.graphql.schema import UsersQuery
|
||||||
from virtualization.graphql.schema import VirtualizationQuery
|
from virtualization.graphql.schema import VirtualizationQuery
|
||||||
|
from wireless.graphql.schema import WirelessQuery
|
||||||
|
|
||||||
|
|
||||||
class Query(
|
class Query(
|
||||||
@ -17,6 +18,7 @@ class Query(
|
|||||||
TenancyQuery,
|
TenancyQuery,
|
||||||
UsersQuery,
|
UsersQuery,
|
||||||
VirtualizationQuery,
|
VirtualizationQuery,
|
||||||
|
WirelessQuery,
|
||||||
graphene.ObjectType
|
graphene.ObjectType
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
@ -176,6 +176,7 @@ CONNECTIONS_MENU = Menu(
|
|||||||
label='Connections',
|
label='Connections',
|
||||||
items=(
|
items=(
|
||||||
get_model_item('dcim', 'cable', 'Cables', actions=['import']),
|
get_model_item('dcim', 'cable', 'Cables', actions=['import']),
|
||||||
|
get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='dcim:interface_connections_list',
|
link='dcim:interface_connections_list',
|
||||||
link_text='Interface Connections',
|
link_text='Interface Connections',
|
||||||
@ -196,6 +197,20 @@ CONNECTIONS_MENU = Menu(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
WIRELESS_MENU = Menu(
|
||||||
|
label='Wireless',
|
||||||
|
icon_class='mdi mdi-wifi',
|
||||||
|
groups=(
|
||||||
|
MenuGroup(
|
||||||
|
label='Wireless',
|
||||||
|
items=(
|
||||||
|
get_model_item('wireless', 'wirelesslan', 'Wireless LANs'),
|
||||||
|
get_model_item('wireless', 'wirelesslangroup', 'Wireless LAN Groups'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
IPAM_MENU = Menu(
|
IPAM_MENU = Menu(
|
||||||
label='IPAM',
|
label='IPAM',
|
||||||
icon_class='mdi mdi-counter',
|
icon_class='mdi mdi-counter',
|
||||||
@ -351,6 +366,7 @@ MENUS = [
|
|||||||
ORGANIZATION_MENU,
|
ORGANIZATION_MENU,
|
||||||
DEVICES_MENU,
|
DEVICES_MENU,
|
||||||
CONNECTIONS_MENU,
|
CONNECTIONS_MENU,
|
||||||
|
WIRELESS_MENU,
|
||||||
IPAM_MENU,
|
IPAM_MENU,
|
||||||
VIRTUALIZATION_MENU,
|
VIRTUALIZATION_MENU,
|
||||||
CIRCUITS_MENU,
|
CIRCUITS_MENU,
|
||||||
|
@ -326,6 +326,7 @@ INSTALLED_APPS = [
|
|||||||
'users',
|
'users',
|
||||||
'utilities',
|
'utilities',
|
||||||
'virtualization',
|
'virtualization',
|
||||||
|
'wireless',
|
||||||
'django_rq', # Must come after extras to allow overriding management commands
|
'django_rq', # Must come after extras to allow overriding management commands
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
]
|
]
|
||||||
|
@ -48,6 +48,7 @@ _patterns = [
|
|||||||
path('tenancy/', include('tenancy.urls')),
|
path('tenancy/', include('tenancy.urls')),
|
||||||
path('user/', include('users.urls')),
|
path('user/', include('users.urls')),
|
||||||
path('virtualization/', include('virtualization.urls')),
|
path('virtualization/', include('virtualization.urls')),
|
||||||
|
path('wireless/', include('wireless.urls')),
|
||||||
|
|
||||||
# API
|
# API
|
||||||
path('api/', APIRootView.as_view(), name='api-root'),
|
path('api/', APIRootView.as_view(), name='api-root'),
|
||||||
@ -58,6 +59,7 @@ _patterns = [
|
|||||||
path('api/tenancy/', include('tenancy.api.urls')),
|
path('api/tenancy/', include('tenancy.api.urls')),
|
||||||
path('api/users/', include('users.api.urls')),
|
path('api/users/', include('users.api.urls')),
|
||||||
path('api/virtualization/', include('virtualization.api.urls')),
|
path('api/virtualization/', include('virtualization.api.urls')),
|
||||||
|
path('api/wireless/', include('wireless.api.urls')),
|
||||||
path('api/status/', StatusView.as_view(), name='api-status'),
|
path('api/status/', StatusView.as_view(), name='api-status'),
|
||||||
path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
|
path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
|
||||||
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),
|
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),
|
||||||
|
@ -27,6 +27,7 @@ from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
|
|||||||
from netbox.forms import SearchForm
|
from netbox.forms import SearchForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
|
|
||||||
|
|
||||||
class HomeView(View):
|
class HomeView(View):
|
||||||
@ -92,14 +93,19 @@ class HomeView(View):
|
|||||||
("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count),
|
("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count),
|
||||||
("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count),
|
("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count),
|
||||||
)
|
)
|
||||||
|
wireless = (
|
||||||
|
("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count),
|
||||||
|
("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count),
|
||||||
|
)
|
||||||
sections = (
|
sections = (
|
||||||
("Organization", org, "domain"),
|
("Organization", org, "domain"),
|
||||||
("IPAM", ipam, "counter"),
|
("IPAM", ipam, "counter"),
|
||||||
("Virtualization", virtualization, "monitor"),
|
("Virtualization", virtualization, "monitor"),
|
||||||
("Inventory", dcim, "server"),
|
("Inventory", dcim, "server"),
|
||||||
("Connections", connections, "cable-data"),
|
|
||||||
("Circuits", circuits, "transit-connection-variant"),
|
("Circuits", circuits, "transit-connection-variant"),
|
||||||
|
("Connections", connections, "cable-data"),
|
||||||
("Power", power, "flash"),
|
("Power", power, "flash"),
|
||||||
|
("Wireless", wireless, "wifi"),
|
||||||
)
|
)
|
||||||
|
|
||||||
stats = []
|
stats = []
|
||||||
|
BIN
netbox/project-static/dist/cable_trace.css
vendored
BIN
netbox/project-static/dist/cable_trace.css
vendored
Binary file not shown.
@ -59,8 +59,13 @@ svg {
|
|||||||
stroke: var(--nbx-trace-cable-shadow);
|
stroke: var(--nbx-trace-cable-shadow);
|
||||||
stroke-width: 7px;
|
stroke-width: 7px;
|
||||||
}
|
}
|
||||||
|
line.wireless-link {
|
||||||
|
stroke: var(--nbx-trace-attachment);
|
||||||
|
stroke-dasharray: 4px 12px;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
line.attachment {
|
line.attachment {
|
||||||
stroke: var(--nbx-trace-attachment);
|
stroke: var(--nbx-trace-attachment);
|
||||||
stroke-dasharray: 5px, 5px;
|
stroke-dasharray: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<span class="text-muted">Marked as connected</span>
|
<span class="text-muted">Marked as connected</span>
|
||||||
{% elif termination.cable %}
|
{% elif termination.cable %}
|
||||||
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
|
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
|
||||||
{% with peer=termination.get_cable_peer %}
|
{% with peer=termination.get_link_peer %}
|
||||||
to
|
to
|
||||||
{% if peer.device %}
|
{% if peer.device %}
|
||||||
<a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a><br/>
|
<a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a><br/>
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% if object.is_connectable %}
|
{% if not object.is_virtual %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Connection
|
Connection
|
||||||
@ -211,10 +211,40 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
{% elif object.wireless_link %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Wireless Link</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ object.wireless_link.get_absolute_url }}">{{ object.wireless_link }}</a>
|
||||||
|
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||||
|
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% with peer_interface=object.connected_endpoint %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Device</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ peer_interface.device.get_absolute_url }}">{{ peer_interface.device }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ peer_interface.get_absolute_url }}">{{ peer_interface }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Type</th>
|
||||||
|
<td>{{ peer_interface.get_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endwith %}
|
||||||
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
{% if perms.dcim.add_cable %}
|
{% if object.is_wired and perms.dcim.add_cable %}
|
||||||
<div class="dropdown float-end">
|
<div class="dropdown float-end">
|
||||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||||
@ -242,12 +272,125 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% elif object.is_wireless and perms.wireless.add_wirelesslink %}
|
||||||
|
<div class="dropdown float-end">
|
||||||
|
<a href="{% url 'wireless:wirelesslink_add' %}?interface_a={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||||
|
<span class="mdi mdi-wifi-plus" aria-hidden="true"></span> Connect
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if object.is_wireless %}
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Wireless</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% with peer=object.connected_endpoint %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Local</th>
|
||||||
|
{% if peer %}
|
||||||
|
<th>Peer</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Role</th>
|
||||||
|
<td>{{ object.get_rf_role_display|placeholder }}</td>
|
||||||
|
{% if peer %}
|
||||||
|
<td>{{ peer.get_rf_role_display|placeholder }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Channel</th>
|
||||||
|
<td>{{ object.get_rf_channel_display|placeholder }}</td>
|
||||||
|
{% if peer %}
|
||||||
|
<td{% if peer.rf_channel != object.rf_channel %} class="text-danger"{% endif %}>
|
||||||
|
{{ peer.get_rf_channel_display|placeholder }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Channel Frequency</th>
|
||||||
|
<td>
|
||||||
|
{% if object.rf_channel_frequency %}
|
||||||
|
{{ object.rf_channel_frequency|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if peer %}
|
||||||
|
<td{% if peer.rf_channel_frequency != object.rf_channel_frequency %} class="text-danger"{% endif %}>
|
||||||
|
{% if peer.rf_channel_frequency %}
|
||||||
|
{{ peer.rf_channel_frequency|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Channel Width</th>
|
||||||
|
<td>
|
||||||
|
{% if object.rf_channel_width %}
|
||||||
|
{{ object.rf_channel_width|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if peer %}
|
||||||
|
<td{% if peer.rf_channel_width != object.rf_channel_width %} class="text-danger"{% endif %}>
|
||||||
|
{% if peer.rf_channel_width %}
|
||||||
|
{{ peer.rf_channel_width|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Wireless LANs</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover table-headings">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>SSID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for wlan in object.wireless_lans.all %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if wlan.group %}
|
||||||
|
<a href="{{ wlan.group.get_absolute_url }}">{{ wlan.group }}</a>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ wlan.get_absolute_url }}">{{ wlan.ssid }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-muted">None</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if object.is_lag %}
|
{% if object.is_lag %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">LAG Members</h5>
|
<h5 class="card-header">LAG Members</h5>
|
||||||
|
@ -29,6 +29,20 @@
|
|||||||
{% render_field form.mark_connected %}
|
{% render_field form.mark_connected %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if form.instance.is_wireless %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Wireless</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.rf_role %}
|
||||||
|
{% render_field form.rf_channel %}
|
||||||
|
{% render_field form.rf_channel_frequency %}
|
||||||
|
{% render_field form.rf_channel_width %}
|
||||||
|
{% render_field form.wireless_lan_group %}
|
||||||
|
{% render_field form.wireless_lans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">802.1Q Switching</h5>
|
<h5 class="offset-sm-3">802.1Q Switching</h5>
|
||||||
|
21
netbox/templates/wireless/inc/authentication_attrs.html
Normal file
21
netbox/templates/wireless/inc/authentication_attrs.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Authentication</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Type</th>
|
||||||
|
<td>{{ object.get_auth_type_display|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Cipher</th>
|
||||||
|
<td>{{ object.get_auth_cipher_display|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">PSK</th>
|
||||||
|
<td class="font-monospace">{{ object.auth_psk|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
54
netbox/templates/wireless/inc/wirelesslink_interface.html
Normal file
54
netbox/templates/wireless/inc/wirelesslink_interface.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tr>
|
||||||
|
<td>Device</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Interface</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ interface.get_absolute_url }}">{{ interface }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>
|
||||||
|
{{ interface.get_type_display }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Role</td>
|
||||||
|
<td>
|
||||||
|
{{ interface.get_rf_role_display|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Channel</td>
|
||||||
|
<td>
|
||||||
|
{{ interface.get_rf_channel_display|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Channel Frequency</th>
|
||||||
|
<td>
|
||||||
|
{% if interface.rf_channel_frequency %}
|
||||||
|
{{ interface.rf_channel_frequency|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Channel Width</th>
|
||||||
|
<td>
|
||||||
|
{% if interface.rf_channel_width %}
|
||||||
|
{{ interface.rf_channel_width|simplify_decimal }} MHz
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
64
netbox/templates/wireless/wirelesslan.html
Normal file
64
netbox/templates/wireless/wirelesslan.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Wireless LAN</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">SSID</th>
|
||||||
|
<td>{{ object.ssid }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Group</td>
|
||||||
|
<td>
|
||||||
|
{% if object.group %}
|
||||||
|
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">VLAN</th>
|
||||||
|
<td>
|
||||||
|
{% if object.vlan %}
|
||||||
|
<a href="{{ object.vlan.get_absolute_url }}">{{ object.vlan }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'wireless/inc/authentication_attrs.html' %}
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Attached Interfaces</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'inc/table.html' with table=interfaces_table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %}
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
73
netbox/templates/wireless/wirelesslangroup.html
Normal file
73
netbox/templates/wireless/wirelesslangroup.html
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% for group in object.get_ancestors %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'wireless:wirelesslangroup_list' %}?parent_id={{ group.pk }}">{{ group }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Wireless LAN Group</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Parent</th>
|
||||||
|
<td>
|
||||||
|
{% if object.parent %}
|
||||||
|
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Wireless LANs</th>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}">{{ wirelesslans_table.rows|length }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Wireless LANs</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'inc/table.html' with table=wirelesslans_table %}
|
||||||
|
</div>
|
||||||
|
{% if perms.wireless.add_wirelesslan %}
|
||||||
|
<div class="card-footer text-end noprint">
|
||||||
|
<a href="{% url 'wireless:wirelesslan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Wireless LAN
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% include 'inc/paginator.html' with paginator=wirelesslans_table.paginator page=wirelesslans_table.page %}
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
55
netbox/templates/wireless/wirelesslink.html
Normal file
55
netbox/templates/wireless/wirelesslink.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Interface A</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_a %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Link Properties</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Status</th>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">SSID</th>
|
||||||
|
<td>{{ object.ssid|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Interface B</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_b %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'wireless/inc/authentication_attrs.html' %}
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
33
netbox/templates/wireless/wirelesslink_edit.html
Normal file
33
netbox/templates/wireless/wirelesslink_edit.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="field-group">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Side A</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.device_a %}
|
||||||
|
{% render_field form.interface_a %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="field-group">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Side B</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.device_b %}
|
||||||
|
{% render_field form.interface_b %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||||
|
</div>
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import decimal
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
@ -146,6 +147,19 @@ def humanize_megabytes(mb):
|
|||||||
return f'{mb} MB'
|
return f'{mb} MB'
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def simplify_decimal(value):
|
||||||
|
"""
|
||||||
|
Return the simplest expression of a decimal value. Examples:
|
||||||
|
1.00 => '1'
|
||||||
|
1.20 => '1.2'
|
||||||
|
1.23 => '1.23'
|
||||||
|
"""
|
||||||
|
if type(value) is not decimal.Decimal:
|
||||||
|
return value
|
||||||
|
return str(value).rstrip('0').rstrip('.')
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def tzoffset(value):
|
def tzoffset(value):
|
||||||
"""
|
"""
|
||||||
|
0
netbox/wireless/__init__.py
Normal file
0
netbox/wireless/__init__.py
Normal file
0
netbox/wireless/api/__init__.py
Normal file
0
netbox/wireless/api/__init__.py
Normal file
36
netbox/wireless/api/nested_serializers.py
Normal file
36
netbox/wireless/api/nested_serializers.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from netbox.api import WritableNestedSerializer
|
||||||
|
from wireless.models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'NestedWirelessLANSerializer',
|
||||||
|
'NestedWirelessLANGroupSerializer',
|
||||||
|
'NestedWirelessLinkSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||||
|
wirelesslan_count = serializers.IntegerField(read_only=True)
|
||||||
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = ['id', 'url', 'display', 'name', 'slug', 'wirelesslan_count', '_depth']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedWirelessLANSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = ['id', 'url', 'display', 'ssid']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedWirelessLinkSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLink
|
||||||
|
fields = ['id', 'url', 'display', 'ssid']
|
59
netbox/wireless/api/serializers.py
Normal file
59
netbox/wireless/api/serializers.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.api.serializers import NestedInterfaceSerializer
|
||||||
|
from ipam.api.serializers import NestedVLANSerializer
|
||||||
|
from netbox.api import ChoiceField
|
||||||
|
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import *
|
||||||
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANGroupSerializer',
|
||||||
|
'WirelessLANSerializer',
|
||||||
|
'WirelessLinkSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupSerializer(NestedGroupModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||||
|
parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
|
wirelesslan_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'wirelesslan_count', '_depth',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
|
||||||
|
group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
|
||||||
|
vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
|
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
|
||||||
|
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
|
||||||
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
|
interface_a = NestedInterfaceSerializer()
|
||||||
|
interface_b = NestedInterfaceSerializer()
|
||||||
|
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
|
||||||
|
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLink
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', 'auth_type',
|
||||||
|
'auth_cipher', 'auth_psk',
|
||||||
|
]
|
13
netbox/wireless/api/urls.py
Normal file
13
netbox/wireless/api/urls.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from netbox.api import OrderedDefaultRouter
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
router = OrderedDefaultRouter()
|
||||||
|
router.APIRootView = views.WirelessRootView
|
||||||
|
|
||||||
|
router.register('wireless-lan-groupss', views.WirelessLANGroupViewSet)
|
||||||
|
router.register('wireless-lans', views.WirelessLANViewSet)
|
||||||
|
router.register('wireless-links', views.WirelessLinkViewSet)
|
||||||
|
|
||||||
|
app_name = 'wireless-api'
|
||||||
|
urlpatterns = router.urls
|
38
netbox/wireless/api/views.py
Normal file
38
netbox/wireless/api/views.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
|
from wireless import filtersets
|
||||||
|
from wireless.models import *
|
||||||
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessRootView(APIRootView):
|
||||||
|
"""
|
||||||
|
Wireless API root view
|
||||||
|
"""
|
||||||
|
def get_view_name(self):
|
||||||
|
return 'Wireless'
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = WirelessLANGroup.objects.add_related_count(
|
||||||
|
WirelessLANGroup.objects.all(),
|
||||||
|
WirelessLAN,
|
||||||
|
'group',
|
||||||
|
'wirelesslan_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
serializer_class = serializers.WirelessLANGroupSerializer
|
||||||
|
filterset_class = filtersets.WirelessLANGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
|
||||||
|
serializer_class = serializers.WirelessLANSerializer
|
||||||
|
filterset_class = filtersets.WirelessLANFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
|
||||||
|
serializer_class = serializers.WirelessLinkSerializer
|
||||||
|
filterset_class = filtersets.WirelessLinkFilterSet
|
8
netbox/wireless/apps.py
Normal file
8
netbox/wireless/apps.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessConfig(AppConfig):
|
||||||
|
name = 'wireless'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import wireless.signals
|
191
netbox/wireless/choices.py
Normal file
191
netbox/wireless/choices.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessRoleChoices(ChoiceSet):
|
||||||
|
ROLE_AP = 'ap'
|
||||||
|
ROLE_STATION = 'station'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(ROLE_AP, 'Access point'),
|
||||||
|
(ROLE_STATION, 'Station'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessChannelChoices(ChoiceSet):
|
||||||
|
|
||||||
|
# 2.4 GHz
|
||||||
|
CHANNEL_24G_1 = '2.4g-1-2412-22'
|
||||||
|
CHANNEL_24G_2 = '2.4g-2-2417-22'
|
||||||
|
CHANNEL_24G_3 = '2.4g-3-2422-22'
|
||||||
|
CHANNEL_24G_4 = '2.4g-4-2427-22'
|
||||||
|
CHANNEL_24G_5 = '2.4g-5-2432-22'
|
||||||
|
CHANNEL_24G_6 = '2.4g-6-2437-22'
|
||||||
|
CHANNEL_24G_7 = '2.4g-7-2442-22'
|
||||||
|
CHANNEL_24G_8 = '2.4g-8-2447-22'
|
||||||
|
CHANNEL_24G_9 = '2.4g-9-2452-22'
|
||||||
|
CHANNEL_24G_10 = '2.4g-10-2457-22'
|
||||||
|
CHANNEL_24G_11 = '2.4g-11-2462-22'
|
||||||
|
CHANNEL_24G_12 = '2.4g-12-2467-22'
|
||||||
|
CHANNEL_24G_13 = '2.4g-13-2472-22'
|
||||||
|
|
||||||
|
# 5 GHz
|
||||||
|
CHANNEL_5G_32 = '5g-32-5160-20'
|
||||||
|
CHANNEL_5G_34 = '5g-34-5170-40'
|
||||||
|
CHANNEL_5G_36 = '5g-36-5180-20'
|
||||||
|
CHANNEL_5G_38 = '5g-38-5190-40'
|
||||||
|
CHANNEL_5G_40 = '5g-40-5200-20'
|
||||||
|
CHANNEL_5G_42 = '5g-42-5210-80'
|
||||||
|
CHANNEL_5G_44 = '5g-44-5220-20'
|
||||||
|
CHANNEL_5G_46 = '5g-46-5230-40'
|
||||||
|
CHANNEL_5G_48 = '5g-48-5240-20'
|
||||||
|
CHANNEL_5G_50 = '5g-50-5250-160'
|
||||||
|
CHANNEL_5G_52 = '5g-52-5260-20'
|
||||||
|
CHANNEL_5G_54 = '5g-54-5270-40'
|
||||||
|
CHANNEL_5G_56 = '5g-56-5280-20'
|
||||||
|
CHANNEL_5G_58 = '5g-58-5290-80'
|
||||||
|
CHANNEL_5G_60 = '5g-60-5300-20'
|
||||||
|
CHANNEL_5G_62 = '5g-62-5310-40'
|
||||||
|
CHANNEL_5G_64 = '5g-64-5320-20'
|
||||||
|
CHANNEL_5G_100 = '5g-100-5500-20'
|
||||||
|
CHANNEL_5G_102 = '5g-102-5510-40'
|
||||||
|
CHANNEL_5G_104 = '5g-104-5520-20'
|
||||||
|
CHANNEL_5G_106 = '5g-106-5530-80'
|
||||||
|
CHANNEL_5G_108 = '5g-108-5540-20'
|
||||||
|
CHANNEL_5G_110 = '5g-110-5550-40'
|
||||||
|
CHANNEL_5G_112 = '5g-112-5560-20'
|
||||||
|
CHANNEL_5G_114 = '5g-114-5570-160'
|
||||||
|
CHANNEL_5G_116 = '5g-116-5580-20'
|
||||||
|
CHANNEL_5G_118 = '5g-118-5590-40'
|
||||||
|
CHANNEL_5G_120 = '5g-120-5600-20'
|
||||||
|
CHANNEL_5G_122 = '5g-122-5610-80'
|
||||||
|
CHANNEL_5G_124 = '5g-124-5620-20'
|
||||||
|
CHANNEL_5G_126 = '5g-126-5630-40'
|
||||||
|
CHANNEL_5G_128 = '5g-128-5640-20'
|
||||||
|
CHANNEL_5G_132 = '5g-132-5660-20'
|
||||||
|
CHANNEL_5G_134 = '5g-134-5670-40'
|
||||||
|
CHANNEL_5G_136 = '5g-136-5680-20'
|
||||||
|
CHANNEL_5G_138 = '5g-138-5690-80'
|
||||||
|
CHANNEL_5G_140 = '5g-140-5700-20'
|
||||||
|
CHANNEL_5G_142 = '5g-142-5710-40'
|
||||||
|
CHANNEL_5G_144 = '5g-144-5720-20'
|
||||||
|
CHANNEL_5G_149 = '5g-149-5745-20'
|
||||||
|
CHANNEL_5G_151 = '5g-151-5755-40'
|
||||||
|
CHANNEL_5G_153 = '5g-153-5765-20'
|
||||||
|
CHANNEL_5G_155 = '5g-155-5775-80'
|
||||||
|
CHANNEL_5G_157 = '5g-157-5785-20'
|
||||||
|
CHANNEL_5G_159 = '5g-159-5795-40'
|
||||||
|
CHANNEL_5G_161 = '5g-161-5805-20'
|
||||||
|
CHANNEL_5G_163 = '5g-163-5815-160'
|
||||||
|
CHANNEL_5G_165 = '5g-165-5825-20'
|
||||||
|
CHANNEL_5G_167 = '5g-167-5835-40'
|
||||||
|
CHANNEL_5G_169 = '5g-169-5845-20'
|
||||||
|
CHANNEL_5G_171 = '5g-171-5855-80'
|
||||||
|
CHANNEL_5G_173 = '5g-173-5865-20'
|
||||||
|
CHANNEL_5G_175 = '5g-175-5875-40'
|
||||||
|
CHANNEL_5G_177 = '5g-177-5885-20'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(
|
||||||
|
'2.4 GHz (802.11b/g/n/ax)',
|
||||||
|
(
|
||||||
|
(CHANNEL_24G_1, '1 (2412 MHz)'),
|
||||||
|
(CHANNEL_24G_2, '2 (2417 MHz)'),
|
||||||
|
(CHANNEL_24G_3, '3 (2422 MHz)'),
|
||||||
|
(CHANNEL_24G_4, '4 (2427 MHz)'),
|
||||||
|
(CHANNEL_24G_5, '5 (2432 MHz)'),
|
||||||
|
(CHANNEL_24G_6, '6 (2437 MHz)'),
|
||||||
|
(CHANNEL_24G_7, '7 (2442 MHz)'),
|
||||||
|
(CHANNEL_24G_8, '8 (2447 MHz)'),
|
||||||
|
(CHANNEL_24G_9, '9 (2452 MHz)'),
|
||||||
|
(CHANNEL_24G_10, '10 (2457 MHz)'),
|
||||||
|
(CHANNEL_24G_11, '11 (2462 MHz)'),
|
||||||
|
(CHANNEL_24G_12, '12 (2467 MHz)'),
|
||||||
|
(CHANNEL_24G_13, '13 (2472 MHz)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'5 GHz (802.11a/n/ac/ax)',
|
||||||
|
(
|
||||||
|
(CHANNEL_5G_32, '32 (5160/20 MHz)'),
|
||||||
|
(CHANNEL_5G_34, '34 (5170/40 MHz)'),
|
||||||
|
(CHANNEL_5G_36, '36 (5180/20 MHz)'),
|
||||||
|
(CHANNEL_5G_38, '38 (5190/40 MHz)'),
|
||||||
|
(CHANNEL_5G_40, '40 (5200/20 MHz)'),
|
||||||
|
(CHANNEL_5G_42, '42 (5210/80 MHz)'),
|
||||||
|
(CHANNEL_5G_44, '44 (5220/20 MHz)'),
|
||||||
|
(CHANNEL_5G_46, '46 (5230/40 MHz)'),
|
||||||
|
(CHANNEL_5G_48, '48 (5240/20 MHz)'),
|
||||||
|
(CHANNEL_5G_50, '50 (5250/160 MHz)'),
|
||||||
|
(CHANNEL_5G_52, '52 (5260/20 MHz)'),
|
||||||
|
(CHANNEL_5G_54, '54 (5270/40 MHz)'),
|
||||||
|
(CHANNEL_5G_56, '56 (5280/20 MHz)'),
|
||||||
|
(CHANNEL_5G_58, '58 (5290/80 MHz)'),
|
||||||
|
(CHANNEL_5G_60, '60 (5300/20 MHz)'),
|
||||||
|
(CHANNEL_5G_62, '62 (5310/40 MHz)'),
|
||||||
|
(CHANNEL_5G_64, '64 (5320/20 MHz)'),
|
||||||
|
(CHANNEL_5G_100, '100 (5500/20 MHz)'),
|
||||||
|
(CHANNEL_5G_102, '102 (5510/40 MHz)'),
|
||||||
|
(CHANNEL_5G_104, '104 (5520/20 MHz)'),
|
||||||
|
(CHANNEL_5G_106, '106 (5530/80 MHz)'),
|
||||||
|
(CHANNEL_5G_108, '108 (5540/20 MHz)'),
|
||||||
|
(CHANNEL_5G_110, '110 (5550/40 MHz)'),
|
||||||
|
(CHANNEL_5G_112, '112 (5560/20 MHz)'),
|
||||||
|
(CHANNEL_5G_114, '114 (5570/160 MHz)'),
|
||||||
|
(CHANNEL_5G_116, '116 (5580/20 MHz)'),
|
||||||
|
(CHANNEL_5G_118, '118 (5590/40 MHz)'),
|
||||||
|
(CHANNEL_5G_120, '120 (5600/20 MHz)'),
|
||||||
|
(CHANNEL_5G_122, '122 (5610/80 MHz)'),
|
||||||
|
(CHANNEL_5G_124, '124 (5620/20 MHz)'),
|
||||||
|
(CHANNEL_5G_126, '126 (5630/40 MHz)'),
|
||||||
|
(CHANNEL_5G_128, '128 (5640/20 MHz)'),
|
||||||
|
(CHANNEL_5G_132, '132 (5660/20 MHz)'),
|
||||||
|
(CHANNEL_5G_134, '134 (5670/40 MHz)'),
|
||||||
|
(CHANNEL_5G_136, '136 (5680/20 MHz)'),
|
||||||
|
(CHANNEL_5G_138, '138 (5690/80 MHz)'),
|
||||||
|
(CHANNEL_5G_140, '140 (5700/20 MHz)'),
|
||||||
|
(CHANNEL_5G_142, '142 (5710/40 MHz)'),
|
||||||
|
(CHANNEL_5G_144, '144 (5720/20 MHz)'),
|
||||||
|
(CHANNEL_5G_149, '149 (5745/20 MHz)'),
|
||||||
|
(CHANNEL_5G_151, '151 (5755/40 MHz)'),
|
||||||
|
(CHANNEL_5G_153, '153 (5765/20 MHz)'),
|
||||||
|
(CHANNEL_5G_155, '155 (5775/80 MHz)'),
|
||||||
|
(CHANNEL_5G_157, '157 (5785/20 MHz)'),
|
||||||
|
(CHANNEL_5G_159, '159 (5795/40 MHz)'),
|
||||||
|
(CHANNEL_5G_161, '161 (5805/20 MHz)'),
|
||||||
|
(CHANNEL_5G_163, '163 (5815/160 MHz)'),
|
||||||
|
(CHANNEL_5G_165, '165 (5825/20 MHz)'),
|
||||||
|
(CHANNEL_5G_167, '167 (5835/40 MHz)'),
|
||||||
|
(CHANNEL_5G_169, '169 (5845/20 MHz)'),
|
||||||
|
(CHANNEL_5G_171, '171 (5855/80 MHz)'),
|
||||||
|
(CHANNEL_5G_173, '173 (5865/20 MHz)'),
|
||||||
|
(CHANNEL_5G_175, '175 (5875/40 MHz)'),
|
||||||
|
(CHANNEL_5G_177, '177 (5885/20 MHz)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessAuthTypeChoices(ChoiceSet):
|
||||||
|
TYPE_OPEN = 'open'
|
||||||
|
TYPE_WEP = 'wep'
|
||||||
|
TYPE_WPA_PERSONAL = 'wpa-personal'
|
||||||
|
TYPE_WPA_ENTERPRISE = 'wpa-enterprise'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(TYPE_OPEN, 'Open'),
|
||||||
|
(TYPE_WEP, 'WEP'),
|
||||||
|
(TYPE_WPA_PERSONAL, 'WPA Personal (PSK)'),
|
||||||
|
(TYPE_WPA_ENTERPRISE, 'WPA Enterprise'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessAuthCipherChoices(ChoiceSet):
|
||||||
|
CIPHER_AUTO = 'auto'
|
||||||
|
CIPHER_TKIP = 'tkip'
|
||||||
|
CIPHER_AES = 'aes'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(CIPHER_AUTO, 'Auto'),
|
||||||
|
(CIPHER_TKIP, 'TKIP'),
|
||||||
|
(CIPHER_AES, 'AES'),
|
||||||
|
)
|
2
netbox/wireless/constants.py
Normal file
2
netbox/wireless/constants.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
SSID_MAX_LENGTH = 32 # Per IEEE 802.11-2007
|
||||||
|
PSK_MAX_LENGTH = 64
|
102
netbox/wireless/filtersets.py
Normal file
102
netbox/wireless/filtersets.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import django_filters
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from extras.filters import TagFilter
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
|
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||||
|
from .choices import *
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANFilterSet',
|
||||||
|
'WirelessLANGroupFilterSet',
|
||||||
|
'WirelessLinkFilterSet',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=WirelessLANGroup.objects.all()
|
||||||
|
)
|
||||||
|
parent = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='parent__slug',
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANFilterSet(PrimaryModelFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
field_name='group',
|
||||||
|
lookup_expr='in'
|
||||||
|
)
|
||||||
|
group = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
field_name='group',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=VLAN.objects.all()
|
||||||
|
)
|
||||||
|
auth_type = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessAuthTypeChoices
|
||||||
|
)
|
||||||
|
auth_cipher = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessAuthCipherChoices
|
||||||
|
)
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = ['id', 'ssid', 'auth_psk']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(ssid__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkFilterSet(PrimaryModelFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=LinkStatusChoices
|
||||||
|
)
|
||||||
|
auth_type = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessAuthTypeChoices
|
||||||
|
)
|
||||||
|
auth_cipher = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=WirelessAuthCipherChoices
|
||||||
|
)
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLink
|
||||||
|
fields = ['id', 'ssid', 'auth_psk']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(ssid__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
4
netbox/wireless/forms/__init__.py
Normal file
4
netbox/wireless/forms/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .models import *
|
||||||
|
from .filtersets import *
|
||||||
|
from .bulk_edit import *
|
||||||
|
from .bulk_import import *
|
101
netbox/wireless/forms/bulk_edit.py
Normal file
101
netbox/wireless/forms/bulk_edit.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from utilities.forms import BootstrapMixin, DynamicModelChoiceField
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.constants import SSID_MAX_LENGTH
|
||||||
|
from wireless.models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANBulkEditForm',
|
||||||
|
'WirelessLANGroupBulkEditForm',
|
||||||
|
'WirelessLinkBulkEditForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLAN.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
group = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
vlan = DynamicModelChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
ssid = forms.CharField(
|
||||||
|
max_length=SSID_MAX_LENGTH,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_type = forms.ChoiceField(
|
||||||
|
choices=WirelessAuthTypeChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_cipher = forms.ChoiceField(
|
||||||
|
choices=WirelessAuthCipherChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_psk = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk']
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLink.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
ssid = forms.CharField(
|
||||||
|
max_length=SSID_MAX_LENGTH,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=LinkStatusChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_type = forms.ChoiceField(
|
||||||
|
choices=WirelessAuthTypeChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_cipher = forms.ChoiceField(
|
||||||
|
choices=WirelessAuthCipherChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
auth_psk = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk']
|
83
netbox/wireless/forms/bulk_import.py
Normal file
83
netbox/wireless/forms/bulk_import.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.models import Interface
|
||||||
|
from extras.forms import CustomFieldModelCSVForm
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANCSVForm',
|
||||||
|
'WirelessLANGroupCSVForm',
|
||||||
|
'WirelessLinkCSVForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||||
|
parent = CSVModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Parent group'
|
||||||
|
)
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = ('name', 'slug', 'parent', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANCSVForm(CustomFieldModelCSVForm):
|
||||||
|
group = CSVModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Assigned group'
|
||||||
|
)
|
||||||
|
vlan = CSVModelChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Bridged VLAN'
|
||||||
|
)
|
||||||
|
auth_type = CSVChoiceField(
|
||||||
|
choices=WirelessAuthTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Authentication type'
|
||||||
|
)
|
||||||
|
auth_cipher = CSVChoiceField(
|
||||||
|
choices=WirelessAuthCipherChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Authentication cipher'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = ('ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkCSVForm(CustomFieldModelCSVForm):
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=LinkStatusChoices,
|
||||||
|
help_text='Connection status'
|
||||||
|
)
|
||||||
|
interface_a = CSVModelChoiceField(
|
||||||
|
queryset=Interface.objects.all()
|
||||||
|
)
|
||||||
|
interface_b = CSVModelChoiceField(
|
||||||
|
queryset=Interface.objects.all()
|
||||||
|
)
|
||||||
|
auth_type = CSVChoiceField(
|
||||||
|
choices=WirelessAuthTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Authentication type'
|
||||||
|
)
|
||||||
|
auth_cipher = CSVChoiceField(
|
||||||
|
choices=WirelessAuthCipherChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Authentication cipher'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLink
|
||||||
|
fields = ('interface_a', 'interface_b', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk')
|
104
netbox/wireless/forms/filtersets.py
Normal file
104
netbox/wireless/forms/filtersets.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from extras.forms import CustomFieldModelFilterForm
|
||||||
|
from utilities.forms import (
|
||||||
|
add_blank_choice, BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField,
|
||||||
|
)
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANFilterForm',
|
||||||
|
'WirelessLANGroupFilterForm',
|
||||||
|
'WirelessLinkFilterForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||||
|
model = WirelessLANGroup
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Parent group'),
|
||||||
|
fetch_trigger='open'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||||
|
model = WirelessLAN
|
||||||
|
field_groups = [
|
||||||
|
('q', 'tag'),
|
||||||
|
('group_id',),
|
||||||
|
]
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
|
ssid = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='SSID'
|
||||||
|
)
|
||||||
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
label=_('Group'),
|
||||||
|
fetch_trigger='open'
|
||||||
|
)
|
||||||
|
auth_type = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=add_blank_choice(WirelessAuthTypeChoices),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
auth_cipher = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=add_blank_choice(WirelessAuthCipherChoices),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
auth_psk = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||||
|
model = WirelessLink
|
||||||
|
field_groups = [
|
||||||
|
['q', 'tag'],
|
||||||
|
]
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
|
ssid = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='SSID'
|
||||||
|
)
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=add_blank_choice(LinkStatusChoices),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
auth_type = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=add_blank_choice(WirelessAuthTypeChoices),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
auth_cipher = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=add_blank_choice(WirelessAuthCipherChoices),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
auth_psk = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
166
netbox/wireless/forms/models.py
Normal file
166
netbox/wireless/forms/models.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
from dcim.models import Device, Interface, Location, Site
|
||||||
|
from extras.forms import CustomFieldModelForm
|
||||||
|
from extras.models import Tag
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from utilities.forms import (
|
||||||
|
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, StaticSelect,
|
||||||
|
)
|
||||||
|
from wireless.models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANForm',
|
||||||
|
'WirelessLANGroupForm',
|
||||||
|
'WirelessLinkForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = [
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
group = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
vlan = DynamicModelChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VLAN'
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = [
|
||||||
|
'ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', 'tags',
|
||||||
|
]
|
||||||
|
fieldsets = (
|
||||||
|
('Wireless LAN', ('ssid', 'group', 'description', 'tags')),
|
||||||
|
('VLAN', ('vlan',)),
|
||||||
|
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
'auth_type': StaticSelect,
|
||||||
|
'auth_cipher': StaticSelect,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
site_a = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Site',
|
||||||
|
initial_params={
|
||||||
|
'devices': '$device_a',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
location_a = DynamicModelChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Location',
|
||||||
|
initial_params={
|
||||||
|
'devices': '$device_a',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
device_a = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_a',
|
||||||
|
'location_id': '$location_a',
|
||||||
|
},
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
initial_params={
|
||||||
|
'interfaces': '$interface_a'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interface_a = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'kind': 'wireless',
|
||||||
|
'device_id': '$device_a',
|
||||||
|
},
|
||||||
|
disabled_indicator='_occupied',
|
||||||
|
label='Interface'
|
||||||
|
)
|
||||||
|
site_b = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Site',
|
||||||
|
initial_params={
|
||||||
|
'devices': '$device_b',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
location_b = DynamicModelChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Location',
|
||||||
|
initial_params={
|
||||||
|
'devices': '$device_b',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
device_b = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_b',
|
||||||
|
'location_id': '$location_b',
|
||||||
|
},
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
initial_params={
|
||||||
|
'interfaces': '$interface_b'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interface_b = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'kind': 'wireless',
|
||||||
|
'device_id': '$device_b',
|
||||||
|
},
|
||||||
|
disabled_indicator='_occupied',
|
||||||
|
label='Interface'
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WirelessLink
|
||||||
|
fields = [
|
||||||
|
'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b',
|
||||||
|
'status', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags',
|
||||||
|
]
|
||||||
|
fieldsets = (
|
||||||
|
('Side A', ('site_a', 'location_a', 'device_a', 'interface_a')),
|
||||||
|
('Side B', ('site_b', 'location_b', 'device_b', 'interface_b')),
|
||||||
|
('Link', ('status', 'ssid', 'description', 'tags')),
|
||||||
|
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
'status': StaticSelect,
|
||||||
|
'auth_type': StaticSelect,
|
||||||
|
'auth_cipher': StaticSelect,
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'auth_type': 'Type',
|
||||||
|
'auth_cipher': 'Cipher',
|
||||||
|
}
|
0
netbox/wireless/graphql/__init__.py
Normal file
0
netbox/wireless/graphql/__init__.py
Normal file
15
netbox/wireless/graphql/schema.py
Normal file
15
netbox/wireless/graphql/schema.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
|
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||||
|
from .types import *
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessQuery(graphene.ObjectType):
|
||||||
|
wireless_lan = ObjectField(WirelessLANType)
|
||||||
|
wireless_lan_list = ObjectListField(WirelessLANType)
|
||||||
|
|
||||||
|
wireless_lan_group = ObjectField(WirelessLANGroupType)
|
||||||
|
wireless_lan_group_list = ObjectListField(WirelessLANGroupType)
|
||||||
|
|
||||||
|
wireless_link = ObjectField(WirelessLinkType)
|
||||||
|
wireless_link_list = ObjectListField(WirelessLinkType)
|
44
netbox/wireless/graphql/types.py
Normal file
44
netbox/wireless/graphql/types.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from wireless import filtersets, models
|
||||||
|
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANType',
|
||||||
|
'WirelessLANGroupType',
|
||||||
|
'WirelessLinkType',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupType(OrganizationalObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.WirelessLANGroup
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.WirelessLANGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANType(PrimaryObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.WirelessLAN
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.WirelessLANFilterSet
|
||||||
|
|
||||||
|
def resolve_auth_type(self, info):
|
||||||
|
return self.auth_type or None
|
||||||
|
|
||||||
|
def resolve_auth_cipher(self, info):
|
||||||
|
return self.auth_cipher or None
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkType(PrimaryObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.WirelessLink
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.WirelessLinkFilterSet
|
||||||
|
|
||||||
|
def resolve_auth_type(self, info):
|
||||||
|
return self.auth_type or None
|
||||||
|
|
||||||
|
def resolve_auth_cipher(self, info):
|
||||||
|
return self.auth_cipher or None
|
80
netbox/wireless/migrations/0001_wireless.py
Normal file
80
netbox/wireless/migrations/0001_wireless.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import django.core.serializers.json
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0139_rename_cable_peer'),
|
||||||
|
('extras', '0062_clear_secrets_changelog'),
|
||||||
|
('ipam', '0050_iprange'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WirelessLANGroup',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('lft', models.PositiveIntegerField(editable=False)),
|
||||||
|
('rght', models.PositiveIntegerField(editable=False)),
|
||||||
|
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('level', models.PositiveIntegerField(editable=False)),
|
||||||
|
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='wireless.wirelesslangroup')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('name', 'pk'),
|
||||||
|
'unique_together': {('parent', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WirelessLAN',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('ssid', models.CharField(max_length=32)),
|
||||||
|
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Wireless LAN',
|
||||||
|
'ordering': ('ssid', 'pk'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WirelessLink',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('ssid', models.CharField(blank=True, max_length=32)),
|
||||||
|
('status', models.CharField(default='connected', max_length=50)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('_interface_a_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')),
|
||||||
|
('_interface_b_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')),
|
||||||
|
('interface_a', models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface')),
|
||||||
|
('interface_b', models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface')),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['pk'],
|
||||||
|
'unique_together': {('interface_a', 'interface_b')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
41
netbox/wireless/migrations/0002_wireless_auth.py
Normal file
41
netbox/wireless/migrations/0002_wireless_auth.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wireless', '0001_wireless'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='auth_cipher',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='auth_psk',
|
||||||
|
field=models.CharField(blank=True, max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='auth_type',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslink',
|
||||||
|
name='auth_cipher',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslink',
|
||||||
|
name='auth_psk',
|
||||||
|
field=models.CharField(blank=True, max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslink',
|
||||||
|
name='auth_type',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
]
|
0
netbox/wireless/migrations/__init__.py
Normal file
0
netbox/wireless/migrations/__init__.py
Normal file
209
netbox/wireless/models.py
Normal file
209
netbox/wireless/models.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.constants import WIRELESS_IFACE_TYPES
|
||||||
|
from extras.utils import extras_features
|
||||||
|
from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel
|
||||||
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
from .choices import *
|
||||||
|
from .constants import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLAN',
|
||||||
|
'WirelessLANGroup',
|
||||||
|
'WirelessLink',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessAuthenticationBase(models.Model):
|
||||||
|
"""
|
||||||
|
Abstract model for attaching attributes related to wireless authentication.
|
||||||
|
"""
|
||||||
|
auth_type = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WirelessAuthTypeChoices,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
auth_cipher = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WirelessAuthCipherChoices,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
auth_psk = models.CharField(
|
||||||
|
max_length=PSK_MAX_LENGTH,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Pre-shared key'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
|
class WirelessLANGroup(NestedGroupModel):
|
||||||
|
"""
|
||||||
|
A nested grouping of WirelessLANs
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
parent = TreeForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='children',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name', 'pk')
|
||||||
|
unique_together = (
|
||||||
|
('parent', 'name')
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('wireless:wirelesslangroup', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
|
class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
|
||||||
|
"""
|
||||||
|
A wireless network formed among an arbitrary number of access point and clients.
|
||||||
|
"""
|
||||||
|
ssid = models.CharField(
|
||||||
|
max_length=SSID_MAX_LENGTH,
|
||||||
|
verbose_name='SSID'
|
||||||
|
)
|
||||||
|
group = models.ForeignKey(
|
||||||
|
to='wireless.WirelessLANGroup',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='wireless_lans',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
vlan = models.ForeignKey(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='VLAN'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('ssid', 'pk')
|
||||||
|
verbose_name = 'Wireless LAN'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.ssid
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('wireless:wirelesslan', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
|
class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
|
||||||
|
"""
|
||||||
|
A point-to-point connection between two wireless Interfaces.
|
||||||
|
"""
|
||||||
|
interface_a = models.ForeignKey(
|
||||||
|
to='dcim.Interface',
|
||||||
|
limit_choices_to={'type__in': WIRELESS_IFACE_TYPES},
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
interface_b = models.ForeignKey(
|
||||||
|
to='dcim.Interface',
|
||||||
|
limit_choices_to={'type__in': WIRELESS_IFACE_TYPES},
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
ssid = models.CharField(
|
||||||
|
max_length=SSID_MAX_LENGTH,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='SSID'
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=LinkStatusChoices,
|
||||||
|
default=LinkStatusChoices.STATUS_CONNECTED
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache the associated device for the A and B interfaces. This enables filtering of WirelessLinks by their
|
||||||
|
# associated Devices.
|
||||||
|
_interface_a_device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_interface_b_device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
clone_fields = ('ssid', 'status')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['pk']
|
||||||
|
unique_together = ('interface_a', 'interface_b')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'#{self.pk}'
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('wireless:wirelesslink', args=[self.pk])
|
||||||
|
|
||||||
|
def get_status_class(self):
|
||||||
|
return LinkStatusChoices.CSS_CLASSES.get(self.status)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate interface types
|
||||||
|
if self.interface_a.type not in WIRELESS_IFACE_TYPES:
|
||||||
|
raise ValidationError({
|
||||||
|
'interface_a': f"{self.interface_a.get_type_display()} is not a wireless interface."
|
||||||
|
})
|
||||||
|
if self.interface_b.type not in WIRELESS_IFACE_TYPES:
|
||||||
|
raise ValidationError({
|
||||||
|
'interface_a': f"{self.interface_b.get_type_display()} is not a wireless interface."
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Store the parent Device for the A and B interfaces
|
||||||
|
self._interface_a_device = self.interface_a.device
|
||||||
|
self._interface_b_device = self.interface_b.device
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
66
netbox/wireless/signals.py
Normal file
66
netbox/wireless/signals.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from dcim.models import CablePath, Interface
|
||||||
|
from dcim.utils import create_cablepath
|
||||||
|
from .models import WirelessLink
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireless links
|
||||||
|
#
|
||||||
|
|
||||||
|
@receiver(post_save, sender=WirelessLink)
|
||||||
|
def update_connected_interfaces(instance, created, raw=False, **kwargs):
|
||||||
|
"""
|
||||||
|
When a WirelessLink is saved, save a reference to it on each connected interface.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('netbox.wireless.wirelesslink')
|
||||||
|
if raw:
|
||||||
|
logger.debug(f"Skipping endpoint updates for imported wireless link {instance}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if instance.interface_a.wireless_link != instance:
|
||||||
|
logger.debug(f"Updating interface A for wireless link {instance}")
|
||||||
|
instance.interface_a.wireless_link = instance
|
||||||
|
instance.interface_a._link_peer = instance.interface_b
|
||||||
|
instance.interface_a.save()
|
||||||
|
if instance.interface_b.cable != instance:
|
||||||
|
logger.debug(f"Updating interface B for wireless link {instance}")
|
||||||
|
instance.interface_b.wireless_link = instance
|
||||||
|
instance.interface_b._link_peer = instance.interface_a
|
||||||
|
instance.interface_b.save()
|
||||||
|
|
||||||
|
# Create/update cable paths
|
||||||
|
if created:
|
||||||
|
for interface in (instance.interface_a, instance.interface_b):
|
||||||
|
create_cablepath(interface)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=WirelessLink)
|
||||||
|
def nullify_connected_interfaces(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When a WirelessLink is deleted, update its two connected Interfaces
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('netbox.wireless.wirelesslink')
|
||||||
|
|
||||||
|
if instance.interface_a is not None:
|
||||||
|
logger.debug(f"Nullifying interface A for wireless link {instance}")
|
||||||
|
Interface.objects.filter(pk=instance.interface_a.pk).update(
|
||||||
|
wireless_link=None,
|
||||||
|
_link_peer_type=None,
|
||||||
|
_link_peer_id=None
|
||||||
|
)
|
||||||
|
if instance.interface_b is not None:
|
||||||
|
logger.debug(f"Nullifying interface B for wireless link {instance}")
|
||||||
|
Interface.objects.filter(pk=instance.interface_b.pk).update(
|
||||||
|
wireless_link=None,
|
||||||
|
_link_peer_type=None,
|
||||||
|
_link_peer_id=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete and retrace any dependent cable paths
|
||||||
|
for cablepath in CablePath.objects.filter(path__contains=instance):
|
||||||
|
cablepath.delete()
|
110
netbox/wireless/tables.py
Normal file
110
netbox/wireless/tables.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from dcim.models import Interface
|
||||||
|
from utilities.tables import (
|
||||||
|
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||||
|
)
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'WirelessLANTable',
|
||||||
|
'WirelessLANGroupTable',
|
||||||
|
'WirelessLinkTable',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = MPTTColumn(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
wirelesslan_count = LinkedCountColumn(
|
||||||
|
viewname='wireless:wirelesslan_list',
|
||||||
|
url_params={'group_id': 'pk'},
|
||||||
|
verbose_name='Wireless LANs'
|
||||||
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='wireless:wirelesslangroup_list'
|
||||||
|
)
|
||||||
|
actions = ButtonsColumn(WirelessLANGroup)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = WirelessLANGroup
|
||||||
|
fields = ('pk', 'name', 'wirelesslan_count', 'description', 'slug', 'tags', 'actions')
|
||||||
|
default_columns = ('pk', 'name', 'wirelesslan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
ssid = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
group = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
interface_count = tables.Column(
|
||||||
|
verbose_name='Interfaces'
|
||||||
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='wireless:wirelesslan_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = WirelessLAN
|
||||||
|
fields = (
|
||||||
|
'pk', 'ssid', 'group', 'description', 'vlan', 'interface_count', 'auth_type', 'auth_cipher', 'auth_psk',
|
||||||
|
'tags',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'auth_type', 'interface_count')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANInterfacesTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
device = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Interface
|
||||||
|
fields = ('pk', 'device', 'name', 'rf_role', 'rf_channel')
|
||||||
|
default_columns = ('pk', 'device', 'name', 'rf_role', 'rf_channel')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
id = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name='ID'
|
||||||
|
)
|
||||||
|
status = ChoiceFieldColumn()
|
||||||
|
device_a = tables.Column(
|
||||||
|
accessor=tables.A('interface_a__device'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
interface_a = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
device_b = tables.Column(
|
||||||
|
accessor=tables.A('interface_b__device'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
interface_b = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='wireless:wirelesslink_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = WirelessLink
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description',
|
||||||
|
'auth_type', 'auth_cipher', 'auth_psk', 'tags',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'auth_type',
|
||||||
|
'description',
|
||||||
|
)
|
0
netbox/wireless/tests/__init__.py
Normal file
0
netbox/wireless/tests/__init__.py
Normal file
141
netbox/wireless/tests/test_api.py
Normal file
141
netbox/wireless/tests/test_api.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import *
|
||||||
|
from dcim.choices import InterfaceTypeChoices
|
||||||
|
from dcim.models import Interface
|
||||||
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
|
|
||||||
|
|
||||||
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
|
def test_root(self):
|
||||||
|
url = reverse('wireless-api:api-root')
|
||||||
|
response = self.client.get('{}?format=api'.format(url), **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = WirelessLANGroup
|
||||||
|
brief_fields = ['_depth', 'display', 'id', 'name', 'slug', 'url', 'wirelesslan_count']
|
||||||
|
create_data = [
|
||||||
|
{
|
||||||
|
'name': 'Wireless LAN Group 4',
|
||||||
|
'slug': 'wireless-lan-group-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Wireless LAN Group 5',
|
||||||
|
'slug': 'wireless-lan-group-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Wireless LAN Group 6',
|
||||||
|
'slug': 'wireless-lan-group-6',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
bulk_update_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
WirelessLANGroup.objects.create(name='Wireless LAN Group 1', slug='wireless-lan-group-1')
|
||||||
|
WirelessLANGroup.objects.create(name='Wireless LAN Group 2', slug='wireless-lan-group-2')
|
||||||
|
WirelessLANGroup.objects.create(name='Wireless LAN Group 3', slug='wireless-lan-group-3')
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = WirelessLAN
|
||||||
|
brief_fields = ['display', 'id', 'ssid', 'url']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
WirelessLANGroup(name='Group 1', slug='group-1'),
|
||||||
|
WirelessLANGroup(name='Group 2', slug='group-2'),
|
||||||
|
WirelessLANGroup(name='Group 3', slug='group-3'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
wireless_lans = (
|
||||||
|
WirelessLAN(ssid='WLAN1'),
|
||||||
|
WirelessLAN(ssid='WLAN2'),
|
||||||
|
WirelessLAN(ssid='WLAN3'),
|
||||||
|
)
|
||||||
|
WirelessLAN.objects.bulk_create(wireless_lans)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'ssid': 'WLAN4',
|
||||||
|
'group': groups[0].pk,
|
||||||
|
'auth_type': WirelessAuthTypeChoices.TYPE_OPEN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'ssid': 'WLAN5',
|
||||||
|
'group': groups[1].pk,
|
||||||
|
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'ssid': 'WLAN6',
|
||||||
|
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
cls.bulk_update_data = {
|
||||||
|
'group': groups[2].pk,
|
||||||
|
'description': 'New description',
|
||||||
|
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
||||||
|
'auth_cipher': WirelessAuthCipherChoices.CIPHER_AES,
|
||||||
|
'auth_psk': 'abc123def456',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = WirelessLink
|
||||||
|
brief_fields = ['display', 'id', 'ssid', 'url']
|
||||||
|
bulk_update_data = {
|
||||||
|
'status': 'planned',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
device = create_test_device('test-device')
|
||||||
|
interfaces = [
|
||||||
|
Interface(
|
||||||
|
device=device,
|
||||||
|
name=f'radio{i}',
|
||||||
|
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||||
|
rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
|
||||||
|
rf_channel_frequency=5160,
|
||||||
|
rf_channel_width=20
|
||||||
|
) for i in range(12)
|
||||||
|
]
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
wireless_links = (
|
||||||
|
WirelessLink(ssid='LINK1', interface_a=interfaces[0], interface_b=interfaces[1]),
|
||||||
|
WirelessLink(ssid='LINK2', interface_a=interfaces[2], interface_b=interfaces[3]),
|
||||||
|
WirelessLink(ssid='LINK3', interface_a=interfaces[4], interface_b=interfaces[5]),
|
||||||
|
)
|
||||||
|
WirelessLink.objects.bulk_create(wireless_links)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'interface_a': interfaces[6].pk,
|
||||||
|
'interface_b': interfaces[7].pk,
|
||||||
|
'ssid': 'LINK4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'interface_a': interfaces[8].pk,
|
||||||
|
'interface_b': interfaces[9].pk,
|
||||||
|
'ssid': 'LINK5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'interface_a': interfaces[10].pk,
|
||||||
|
'interface_b': interfaces[11].pk,
|
||||||
|
'ssid': 'LINK6',
|
||||||
|
},
|
||||||
|
]
|
194
netbox/wireless/tests/test_filtersets.py
Normal file
194
netbox/wireless/tests/test_filtersets.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
||||||
|
from dcim.models import Interface
|
||||||
|
from ipam.models import VLAN
|
||||||
|
from wireless.choices import *
|
||||||
|
from wireless.filtersets import *
|
||||||
|
from wireless.models import *
|
||||||
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = WirelessLANGroup.objects.all()
|
||||||
|
filterset = WirelessLANGroupFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
child_groups = (
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0]),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0]),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=groups[2]),
|
||||||
|
)
|
||||||
|
for group in child_groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_slug(self):
|
||||||
|
params = {'slug': ['wireless-lan-group-1', 'wireless-lan-group-2']}
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_parent(self):
|
||||||
|
parent_groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2]
|
||||||
|
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
filterset = WirelessLANFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
vlans = (
|
||||||
|
VLAN(name='VLAN1', vid=1),
|
||||||
|
VLAN(name='VLAN2', vid=2),
|
||||||
|
VLAN(name='VLAN3', vid=3),
|
||||||
|
)
|
||||||
|
VLAN.objects.bulk_create(vlans)
|
||||||
|
|
||||||
|
wireless_lans = (
|
||||||
|
WirelessLAN(ssid='WLAN1', group=groups[0], vlan=vlans[0], auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1'),
|
||||||
|
WirelessLAN(ssid='WLAN2', group=groups[1], vlan=vlans[1], auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2'),
|
||||||
|
WirelessLAN(ssid='WLAN3', group=groups[2], vlan=vlans[2], auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_psk='PSK3'),
|
||||||
|
)
|
||||||
|
WirelessLAN.objects.bulk_create(wireless_lans)
|
||||||
|
|
||||||
|
def test_ssid(self):
|
||||||
|
params = {'ssid': ['WLAN1', 'WLAN2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_group(self):
|
||||||
|
groups = WirelessLANGroup.objects.all()[:2]
|
||||||
|
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_vlan(self):
|
||||||
|
vlans = VLAN.objects.all()[:2]
|
||||||
|
params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_type(self):
|
||||||
|
params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_cipher(self):
|
||||||
|
params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_psk(self):
|
||||||
|
params = {'auth_psk': ['PSK1', 'PSK2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
filterset = WirelessLinkFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('device1'),
|
||||||
|
create_test_device('device2'),
|
||||||
|
create_test_device('device3'),
|
||||||
|
create_test_device('device4'),
|
||||||
|
)
|
||||||
|
|
||||||
|
interfaces = (
|
||||||
|
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
# Wireless links
|
||||||
|
WirelessLink(
|
||||||
|
interface_a=interfaces[0],
|
||||||
|
interface_b=interfaces[2],
|
||||||
|
ssid='LINK1',
|
||||||
|
status=LinkStatusChoices.STATUS_CONNECTED,
|
||||||
|
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
||||||
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
||||||
|
auth_psk='PSK1'
|
||||||
|
).save()
|
||||||
|
WirelessLink(
|
||||||
|
interface_a=interfaces[1],
|
||||||
|
interface_b=interfaces[3],
|
||||||
|
ssid='LINK2',
|
||||||
|
status=LinkStatusChoices.STATUS_PLANNED,
|
||||||
|
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
||||||
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
||||||
|
auth_psk='PSK2'
|
||||||
|
).save()
|
||||||
|
WirelessLink(
|
||||||
|
interface_a=interfaces[4],
|
||||||
|
interface_b=interfaces[6],
|
||||||
|
ssid='LINK3',
|
||||||
|
status=LinkStatusChoices.STATUS_DECOMMISSIONING,
|
||||||
|
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
||||||
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
|
||||||
|
auth_psk='PSK3'
|
||||||
|
).save()
|
||||||
|
WirelessLink(
|
||||||
|
interface_a=interfaces[5],
|
||||||
|
interface_b=interfaces[7],
|
||||||
|
ssid='LINK4'
|
||||||
|
).save()
|
||||||
|
|
||||||
|
def test_ssid(self):
|
||||||
|
params = {'ssid': ['LINK1', 'LINK2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_status(self):
|
||||||
|
params = {'status': [LinkStatusChoices.STATUS_PLANNED, LinkStatusChoices.STATUS_DECOMMISSIONING]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_type(self):
|
||||||
|
params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_cipher(self):
|
||||||
|
params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_auth_psk(self):
|
||||||
|
params = {'auth_psk': ['PSK1', 'PSK2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
123
netbox/wireless/tests/test_views.py
Normal file
123
netbox/wireless/tests/test_views.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
from wireless.choices import *
|
||||||
|
from wireless.models import *
|
||||||
|
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
||||||
|
from dcim.models import Interface
|
||||||
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
|
model = WirelessLANGroup
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Wireless LAN Group X',
|
||||||
|
'slug': 'wireless-lan-group-x',
|
||||||
|
'parent': groups[2].pk,
|
||||||
|
'description': 'A new wireless LAN group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,slug,description",
|
||||||
|
"Wireles sLAN Group 4,wireless-lan-group-4,Fourth wireless LAN group",
|
||||||
|
"Wireless LAN Group 5,wireless-lan-group-5,Fifth wireless LAN group",
|
||||||
|
"Wireless LAN Group 6,wireless-lan-group-6,Sixth wireless LAN group",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = WirelessLAN
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
|
||||||
|
WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
WirelessLAN.objects.bulk_create([
|
||||||
|
WirelessLAN(group=groups[0], ssid='WLAN1'),
|
||||||
|
WirelessLAN(group=groups[0], ssid='WLAN2'),
|
||||||
|
WirelessLAN(group=groups[0], ssid='WLAN3'),
|
||||||
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'ssid': 'WLAN2',
|
||||||
|
'group': groups[1].pk,
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"group,ssid",
|
||||||
|
"Wireless LAN Group 2,WLAN4",
|
||||||
|
"Wireless LAN Group 2,WLAN5",
|
||||||
|
"Wireless LAN Group 2,WLAN6",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = WirelessLink
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
device = create_test_device('test-device')
|
||||||
|
interfaces = [
|
||||||
|
Interface(
|
||||||
|
device=device,
|
||||||
|
name=f'radio{i}',
|
||||||
|
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||||
|
rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
|
||||||
|
rf_channel_frequency=5160,
|
||||||
|
rf_channel_width=20
|
||||||
|
) for i in range(12)
|
||||||
|
]
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
WirelessLink(interface_a=interfaces[0], interface_b=interfaces[1], ssid='LINK1').save()
|
||||||
|
WirelessLink(interface_a=interfaces[2], interface_b=interfaces[3], ssid='LINK2').save()
|
||||||
|
WirelessLink(interface_a=interfaces[4], interface_b=interfaces[5], ssid='LINK3').save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'interface_a': interfaces[6].pk,
|
||||||
|
'interface_b': interfaces[7].pk,
|
||||||
|
'status': LinkStatusChoices.STATUS_PLANNED,
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"interface_a,interface_b,status",
|
||||||
|
f"{interfaces[6].pk},{interfaces[7].pk},connected",
|
||||||
|
f"{interfaces[8].pk},{interfaces[9].pk},connected",
|
||||||
|
f"{interfaces[10].pk},{interfaces[11].pk},connected",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'status': LinkStatusChoices.STATUS_PLANNED,
|
||||||
|
}
|
45
netbox/wireless/urls.py
Normal file
45
netbox/wireless/urls.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||||
|
from . import views
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
app_name = 'wireless'
|
||||||
|
urlpatterns = (
|
||||||
|
|
||||||
|
# Wireless LAN groups
|
||||||
|
path('wireless-lan-groups/', views.WirelessLANGroupListView.as_view(), name='wirelesslangroup_list'),
|
||||||
|
path('wireless-lan-groups/add/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_add'),
|
||||||
|
path('wireless-lan-groups/import/', views.WirelessLANGroupBulkImportView.as_view(), name='wirelesslangroup_import'),
|
||||||
|
path('wireless-lan-groups/edit/', views.WirelessLANGroupBulkEditView.as_view(), name='wirelesslangroup_bulk_edit'),
|
||||||
|
path('wireless-lan-groups/delete/', views.WirelessLANGroupBulkDeleteView.as_view(), name='wirelesslangroup_bulk_delete'),
|
||||||
|
path('wireless-lan-groups/<int:pk>/', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'),
|
||||||
|
path('wireless-lan-groups/<int:pk>/edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'),
|
||||||
|
path('wireless-lan-groups/<int:pk>/delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'),
|
||||||
|
path('wireless-lan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslangroup_changelog', kwargs={'model': WirelessLANGroup}),
|
||||||
|
|
||||||
|
# Wireless LANs
|
||||||
|
path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'),
|
||||||
|
path('wireless-lans/add/', views.WirelessLANEditView.as_view(), name='wirelesslan_add'),
|
||||||
|
path('wireless-lans/import/', views.WirelessLANBulkImportView.as_view(), name='wirelesslan_import'),
|
||||||
|
path('wireless-lans/edit/', views.WirelessLANBulkEditView.as_view(), name='wirelesslan_bulk_edit'),
|
||||||
|
path('wireless-lans/delete/', views.WirelessLANBulkDeleteView.as_view(), name='wirelesslan_bulk_delete'),
|
||||||
|
path('wireless-lans/<int:pk>/', views.WirelessLANView.as_view(), name='wirelesslan'),
|
||||||
|
path('wireless-lans/<int:pk>/edit/', views.WirelessLANEditView.as_view(), name='wirelesslan_edit'),
|
||||||
|
path('wireless-lans/<int:pk>/delete/', views.WirelessLANDeleteView.as_view(), name='wirelesslan_delete'),
|
||||||
|
path('wireless-lans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}),
|
||||||
|
path('wireless-lans/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}),
|
||||||
|
|
||||||
|
# Wireless links
|
||||||
|
path('wireless-links/', views.WirelessLinkListView.as_view(), name='wirelesslink_list'),
|
||||||
|
path('wireless-links/add/', views.WirelessLinkEditView.as_view(), name='wirelesslink_add'),
|
||||||
|
path('wireless-links/import/', views.WirelessLinkBulkImportView.as_view(), name='wirelesslink_import'),
|
||||||
|
path('wireless-links/edit/', views.WirelessLinkBulkEditView.as_view(), name='wirelesslink_bulk_edit'),
|
||||||
|
path('wireless-links/delete/', views.WirelessLinkBulkDeleteView.as_view(), name='wirelesslink_bulk_delete'),
|
||||||
|
path('wireless-links/<int:pk>/', views.WirelessLinkView.as_view(), name='wirelesslink'),
|
||||||
|
path('wireless-links/<int:pk>/edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'),
|
||||||
|
path('wireless-links/<int:pk>/delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'),
|
||||||
|
path('wireless-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}),
|
||||||
|
path('wireless-links/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}),
|
||||||
|
|
||||||
|
)
|
27
netbox/wireless/utils.py
Normal file
27
netbox/wireless/utils.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from .choices import WirelessChannelChoices
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'get_channel_attr',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_attr(channel, attr):
|
||||||
|
"""
|
||||||
|
Return the specified attribute of a given WirelessChannelChoices value.
|
||||||
|
"""
|
||||||
|
if channel not in WirelessChannelChoices.values():
|
||||||
|
raise ValueError(f"Invalid channel value: {channel}")
|
||||||
|
|
||||||
|
channel_values = channel.split('-')
|
||||||
|
attrs = {
|
||||||
|
'band': channel_values[0],
|
||||||
|
'id': int(channel_values[1]),
|
||||||
|
'frequency': Decimal(channel_values[2]),
|
||||||
|
'width': Decimal(channel_values[3]),
|
||||||
|
}
|
||||||
|
if attr not in attrs:
|
||||||
|
raise ValueError(f"Invalid channel attribute: {attr}")
|
||||||
|
|
||||||
|
return attrs[attr]
|
177
netbox/wireless/views.py
Normal file
177
netbox/wireless/views.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
from dcim.models import Interface
|
||||||
|
from netbox.views import generic
|
||||||
|
from utilities.tables import paginate_table
|
||||||
|
from utilities.utils import count_related
|
||||||
|
from . import filtersets, forms, tables
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireless LAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class WirelessLANGroupListView(generic.ObjectListView):
|
||||||
|
queryset = WirelessLANGroup.objects.add_related_count(
|
||||||
|
WirelessLANGroup.objects.all(),
|
||||||
|
WirelessLAN,
|
||||||
|
'group',
|
||||||
|
'wirelesslan_count',
|
||||||
|
cumulative=True
|
||||||
|
).prefetch_related('tags')
|
||||||
|
filterset = filtersets.WirelessLANGroupFilterSet
|
||||||
|
filterset_form = forms.WirelessLANGroupFilterForm
|
||||||
|
table = tables.WirelessLANGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupView(generic.ObjectView):
|
||||||
|
queryset = WirelessLANGroup.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter(
|
||||||
|
group=instance
|
||||||
|
)
|
||||||
|
wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',))
|
||||||
|
paginate_table(wirelesslans_table, request)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'wirelesslans_table': wirelesslans_table,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupEditView(generic.ObjectEditView):
|
||||||
|
queryset = WirelessLANGroup.objects.all()
|
||||||
|
model_form = forms.WirelessLANGroupForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = WirelessLANGroup.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = WirelessLANGroup.objects.all()
|
||||||
|
model_form = forms.WirelessLANGroupCSVForm
|
||||||
|
table = tables.WirelessLANGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = WirelessLANGroup.objects.add_related_count(
|
||||||
|
WirelessLANGroup.objects.all(),
|
||||||
|
WirelessLAN,
|
||||||
|
'group',
|
||||||
|
'wirelesslan_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
filterset = filtersets.WirelessLANGroupFilterSet
|
||||||
|
table = tables.WirelessLANGroupTable
|
||||||
|
form = forms.WirelessLANGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = WirelessLANGroup.objects.add_related_count(
|
||||||
|
WirelessLANGroup.objects.all(),
|
||||||
|
WirelessLAN,
|
||||||
|
'group',
|
||||||
|
'wirelesslan_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
filterset = filtersets.WirelessLANGroupFilterSet
|
||||||
|
table = tables.WirelessLANGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireless LANs
|
||||||
|
#
|
||||||
|
|
||||||
|
class WirelessLANListView(generic.ObjectListView):
|
||||||
|
queryset = WirelessLAN.objects.annotate(
|
||||||
|
interface_count=count_related(Interface, 'wireless_lans')
|
||||||
|
)
|
||||||
|
filterset = filtersets.WirelessLANFilterSet
|
||||||
|
filterset_form = forms.WirelessLANFilterForm
|
||||||
|
table = tables.WirelessLANTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANView(generic.ObjectView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
attached_interfaces = Interface.objects.restrict(request.user, 'view').filter(
|
||||||
|
wireless_lans=instance
|
||||||
|
)
|
||||||
|
interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces)
|
||||||
|
paginate_table(interfaces_table, request)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'interfaces_table': interfaces_table,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANEditView(generic.ObjectEditView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
model_form = forms.WirelessLANForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
model_form = forms.WirelessLANCSVForm
|
||||||
|
table = tables.WirelessLANTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
filterset = filtersets.WirelessLANFilterSet
|
||||||
|
table = tables.WirelessLANTable
|
||||||
|
form = forms.WirelessLANBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLANBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = WirelessLAN.objects.all()
|
||||||
|
filterset = filtersets.WirelessLANFilterSet
|
||||||
|
table = tables.WirelessLANTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireless Links
|
||||||
|
#
|
||||||
|
|
||||||
|
class WirelessLinkListView(generic.ObjectListView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
filterset = filtersets.WirelessLinkFilterSet
|
||||||
|
filterset_form = forms.WirelessLinkFilterForm
|
||||||
|
table = tables.WirelessLinkTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkView(generic.ObjectView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkEditView(generic.ObjectEditView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
model_form = forms.WirelessLinkForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
model_form = forms.WirelessLinkCSVForm
|
||||||
|
table = tables.WirelessLinkTable
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
filterset = filtersets.WirelessLinkFilterSet
|
||||||
|
table = tables.WirelessLinkTable
|
||||||
|
form = forms.WirelessLinkBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessLinkBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = WirelessLink.objects.all()
|
||||||
|
filterset = filtersets.WirelessLinkFilterSet
|
||||||
|
table = tables.WirelessLinkTable
|
Loading…
Reference in New Issue
Block a user