Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8e1535f
Add RF channel fields to Interface
jeremystretch Oct 12, 2021
8b80b0c
Introduce the wireless app and SSID model
jeremystretch Oct 12, 2021
38f6d22
Enable attachment of wireless interfaces to SSIDs
jeremystretch Oct 12, 2021
5271680
Rename SSID model to WirelessLAN
jeremystretch Oct 12, 2021
90e9f34
Add WirelessLink model
jeremystretch Oct 13, 2021
445e16f
Reference WirelessLink on both attached Interfaces
jeremystretch Oct 13, 2021
138af27
Record wireless links as part of cable path
jeremystretch Oct 13, 2021
1c73bd5
Resolve test errors
jeremystretch Oct 13, 2021
ac2cd55
Rename cable_peer fields to link_peer
jeremystretch Oct 13, 2021
ec0560a
Fix trace_paths command for wireless links
jeremystretch Oct 13, 2021
95ed07a
Add status field to WirelessLink
jeremystretch Oct 13, 2021
43f2d4a
Add SVG trace support for WirelessLinks
jeremystretch Oct 13, 2021
01f791a
Add WirelessLANGroup model
jeremystretch Oct 13, 2021
438b4b4
Add rf_role to Interface
jeremystretch Oct 14, 2021
4c475c1
Extend wireless channel choices
jeremystretch Oct 14, 2021
bdf3594
Include WirelessLAN attached interfaces
jeremystretch Oct 14, 2021
fb9da87
Add devices to WirelessLinkForm
jeremystretch Oct 14, 2021
909b83c
Include interface RF attributes on wireless link view
jeremystretch Oct 14, 2021
64dad7d
Optimize migrations
jeremystretch Oct 14, 2021
6af5a88
Merge branch 'feature' into 3979-wireless
jeremystretch Oct 14, 2021
01d3c06
Move wireless field choices to wireless app
jeremystretch Oct 15, 2021
b7317bf
Remove choices from rf_channel_width
jeremystretch Oct 15, 2021
075f490
Store channel frequency & width as independent values
jeremystretch Oct 15, 2021
717fd76
#3979: UI cleanup
jeremystretch Oct 15, 2021
0c72c20
Add WirelessLANs column to interfaces table
jeremystretch Oct 18, 2021
a665012
Add wireless authentication attributes
jeremystretch Oct 20, 2021
4a71593
Add wireless documentation
jeremystretch Oct 20, 2021
6a4becf
Add tests for wireless
jeremystretch Oct 20, 2021
3a3ed8b
Merge branch 'feature' into 3979-wireless
jeremystretch Oct 21, 2021
1c6a846
#3979 cleanup
jeremystretch Oct 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/core-functionality/wireless.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Wireless Networks

{!models/wireless/wirelesslan.md!}
{!models/wireless/wirelesslangroup.md!}

---

{!models/wireless/wirelesslink.md!}
11 changes: 11 additions & 0 deletions docs/models/dcim/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### 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 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 changes: 11 additions & 0 deletions docs/models/wireless/wirelesslan.md
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions docs/models/wireless/wirelesslangroup.md
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions docs/models/wireless/wirelesslink.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ nav:
- Virtualization: 'core-functionality/virtualization.md'
- Service Mapping: 'core-functionality/services.md'
- Circuits: 'core-functionality/circuits.md'
- Wireless: 'core-functionality/wireless.md'
- Power Tracking: 'core-functionality/power.md'
- Tenancy: 'core-functionality/tenancy.md'
- Contacts: 'core-functionality/contacts.md'
Expand Down
6 changes: 3 additions & 3 deletions netbox/circuits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from circuits.choices import CircuitStatusChoices
from circuits.models import *
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.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
Expand Down Expand Up @@ -88,7 +88,7 @@ class Meta:
]


class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer):
class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False, allow_null=True)
Expand All @@ -99,6 +99,6 @@ class Meta:
model = CircuitTermination
fields = [
'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',
]
21 changes: 21 additions & 0 deletions netbox/circuits/migrations/0004_rename_cable_peer.py
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 2 deletions netbox/circuits/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.urls import reverse

from dcim.fields import ASNField
from dcim.models import CableTermination, PathEndpoint
from dcim.models import LinkTermination, PathEndpoint
from extras.models import ObjectChange
from extras.utils import extras_features
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
Expand Down Expand Up @@ -256,7 +256,7 @@ def get_status_class(self):


@extras_features('webhooks')
class CircuitTermination(ChangeLoggedModel, CableTermination):
class CircuitTermination(ChangeLoggedModel, LinkTermination):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,
Expand Down
65 changes: 34 additions & 31 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,29 @@
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedClusterSerializer
from wireless.choices import *
from .nested_serializers import *


class CableTerminationSerializer(serializers.ModelSerializer):
cable_peer_type = serializers.SerializerMethodField(read_only=True)
cable_peer = serializers.SerializerMethodField(read_only=True)
class LinkTerminationSerializer(serializers.ModelSerializer):
link_peer_type = serializers.SerializerMethodField(read_only=True)
link_peer = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True)

def get_cable_peer_type(self, obj):
if obj._cable_peer is not None:
return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}'
def get_link_peer_type(self, obj):
if obj._link_peer is not None:
return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}'
return None

@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:
serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested')
if obj._link_peer is not None:
serializer = get_serializer_for_model(obj._link_peer, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj._cable_peer, context=context).data
return serializer(obj._link_peer, context=context).data
return None

@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
Expand Down Expand Up @@ -503,7 +504,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
# Device components
#

class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
Expand All @@ -522,12 +523,12 @@ class Meta:
model = ConsoleServerPort
fields = [
'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',
]


class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
Expand All @@ -546,12 +547,12 @@ class Meta:
model = ConsolePort
fields = [
'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',
]


class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
Expand All @@ -575,12 +576,12 @@ class Meta:
model = PowerOutlet
fields = [
'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',
]


class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
Expand All @@ -594,18 +595,20 @@ class Meta:
model = PowerPort
fields = [
'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',
]


class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
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)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
Expand All @@ -620,10 +623,10 @@ class Meta:
model = Interface
fields = [
'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',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'_occupied',
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer',
'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
]

def validate(self, data):
Expand All @@ -640,7 +643,7 @@ def validate(self, data):
return super().validate(data)


class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices)
Expand All @@ -650,7 +653,7 @@ class Meta:
model = RearPort
fields = [
'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',
]

Expand All @@ -666,7 +669,7 @@ class Meta:
fields = ['id', 'url', 'display', 'name', 'label']


class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices)
Expand All @@ -677,7 +680,7 @@ class Meta:
model = FrontPort
fields = [
'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',
]

Expand Down Expand Up @@ -728,7 +731,7 @@ class CableSerializer(PrimaryModelSerializer):
)
termination_a = 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)
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)

Expand Down Expand Up @@ -853,7 +856,7 @@ class Meta:
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')
power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer(
Expand Down Expand Up @@ -883,7 +886,7 @@ class Meta:
model = PowerFeed
fields = [
'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',
'created', 'last_updated', '_occupied',
]
12 changes: 6 additions & 6 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,38 +513,38 @@ def napalm(self, request, pk):
#

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
filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device']


class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related(
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
'device', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet
brief_prefetch_fields = ['device']


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
filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device']


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
filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device']


class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
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
filterset_class = filtersets.InterfaceFilterSet
Expand Down Expand Up @@ -625,7 +625,7 @@ class PowerPanelViewSet(ModelViewSet):

class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
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
filterset_class = filtersets.PowerFeedFilterSet
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1061,7 +1061,7 @@ class PortTypeChoices(ChoiceSet):


#
# Cables
# Cables/links
#

class CableTypeChoices(ChoiceSet):
Expand Down Expand Up @@ -1125,7 +1125,7 @@ class CableTypeChoices(ChoiceSet):
)


class CableStatusChoices(ChoiceSet):
class LinkStatusChoices(ChoiceSet):

STATUS_CONNECTED = 'connected'
STATUS_PLANNED = 'planned'
Expand Down
Loading