Skip to content

Commit

Permalink
Merge pull request #23 from djothi/logicalinterface
Browse files Browse the repository at this point in the history
feat: add LogicalInterface model
  • Loading branch information
djothi authored Jan 4, 2024
2 parents a606411 + 5e197b6 commit 21c3003
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 0 deletions.
39 changes: 39 additions & 0 deletions netbox_cmdb/netbox_cmdb/migrations/0039_logicalinterface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('ipam', '0060_alter_l2vpn_slug'),
('netbox_cmdb', '0038_alter_vrf_unique_together_remove_vrf_device'),
]

operations = [
migrations.CreateModel(
name='LogicalInterface',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('index', models.PositiveSmallIntegerField()),
('enabled', models.BooleanField(default=True)),
('state', models.CharField(default='staging', max_length=50)),
('monitoring_state', models.CharField(default='disabled', max_length=50)),
('mtu', models.PositiveIntegerField(blank=True, null=True)),
('type', models.CharField(default=None, max_length=2)),
('mode', models.CharField(blank=True, default=None, max_length=20, null=True)),
('description', models.CharField(blank=True, max_length=100, null=True)),
('ipv4_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_ipv4_address', to='ipam.ipaddress')),
('ipv6_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_ipv6_address', to='ipam.ipaddress')),
('native_vlan', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_native_vlan', to='netbox_cmdb.vlan')),
('parent_interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s', to='netbox_cmdb.deviceinterface')),
('tagged_vlans', models.ManyToManyField(blank=True, default=None, related_name='%(class)s_tagged_vlans', to='netbox_cmdb.vlan')),
('untagged_vlan', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_untagged_vlan', to='netbox_cmdb.vlan')),
('vrf', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_vrf', to='netbox_cmdb.vrf')),
],
options={
'unique_together': {('index', 'parent_interface')},
},
),
]
104 changes: 104 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/interface.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
from django.db import models
from netbox_cmdb.choices import AssetStateChoices, AssetMonitoringStateChoices
from netbox.models import ChangeLoggedModel
from django.core.exceptions import ValidationError

FEC_CHOICES = [
(None, "None"),
("rs", "Reed Solomon"),
("fc", "FireCode"),
]

LOGICAL_INTERFACE_TYPE_CHOICES = [
("l1", "L1"),
("l2", "L2"),
("l3", "L3"),
]
LOGICAL_INTERFACE_MODE_CHOICES = [
(None, "None"),
("access", "Access"),
("tagged", "Tagged"),
]


class DeviceInterface(ChangeLoggedModel):
"""A device interface configuration."""
Expand Down Expand Up @@ -52,3 +64,95 @@ def __str__(self):

class Meta:
unique_together = ("device", "name")


class LogicalInterface(ChangeLoggedModel):
"""A logical interface configuration."""

index = models.PositiveSmallIntegerField()
enabled = models.BooleanField(default=True)
state = models.CharField(
max_length=50,
choices=AssetStateChoices,
default=AssetStateChoices.STATE_STAGING,
help_text="State of this LogicalInterface",
)
monitoring_state = models.CharField(
max_length=50,
choices=AssetMonitoringStateChoices,
default=AssetMonitoringStateChoices.DISABLED,
help_text="Monitoring state of this LogicalInterface",
)
parent_interface = models.ForeignKey(
to="DeviceInterface", related_name="%(class)s", on_delete=models.CASCADE
)
mtu = models.PositiveIntegerField(blank=True, null=True)
type = models.CharField(
choices=LOGICAL_INTERFACE_TYPE_CHOICES,
max_length=2,
default=None,
)
vrf = models.ForeignKey(
to="VRF", related_name="%(class)s_vrf", on_delete=models.CASCADE, blank=True, null=True
)
ipv4_address = models.ForeignKey(
to="ipam.IPAddress",
related_name="%(class)s_ipv4_address",
on_delete=models.CASCADE,
blank=True,
null=True,
)
ipv6_address = models.ForeignKey(
to="ipam.IPAddress",
related_name="%(class)s_ipv6_address",
on_delete=models.CASCADE,
blank=True,
null=True,
)
mode = models.CharField(
choices=LOGICAL_INTERFACE_MODE_CHOICES,
blank=True,
null=True,
default=None,
max_length=20,
help_text="Interface mode (802.1Q)",
)
untagged_vlan = models.ForeignKey(
to="VLAN",
related_name="%(class)s_untagged_vlan",
on_delete=models.CASCADE,
blank=True,
null=True,
default=None,
)
tagged_vlans = models.ManyToManyField(
to="VLAN", related_name="%(class)s_tagged_vlans", blank=True, default=None
)
native_vlan = models.ForeignKey(
to="VLAN",
related_name="%(class)s_native_vlan",
on_delete=models.CASCADE,
blank=True,
null=True,
default=None,
)
description = models.CharField(max_length=100, blank=True, null=True)

def __str__(self):
return f"{self.parent_interface.name}--{self.index}"

def clean(self):
# List of checks to perform
if self.untagged_vlan and (self.tagged_vlans.exists() or self.native_vlan):
raise ValidationError(
"Untagged VLAN cannot be combined with tagged VLANs or native VLAN."
)

super(LogicalInterface, self).clean()

def save(self, *args, **kwargs):
self.full_clean()
super(LogicalInterface, self).save(*args, **kwargs)

class Meta:
unique_together = ("index", "parent_interface")
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from django.forms import ValidationError
from django.test import TestCase

from netbox_cmdb.models import VLAN, DeviceInterface, LogicalInterface


class BaseTestCase(TestCase):
def setUp(self):
site = Site.objects.create(name="SiteTest", slug="site-test")
manufacturer = Manufacturer.objects.create(name="test", slug="test")
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model="model-test", slug="model-test"
)
device_role = DeviceRole.objects.create(name="role-test", slug="role-test")
device = Device.objects.create(
name="router-test",
device_role=device_role,
device_type=device_type,
site=site,
)

DeviceInterface.objects.create(
name="etp1",
enabled=True,
state="staging",
monitoring_state="warning",
device=device,
autonegotiation=True,
speed=100000,
fec="rs",
description="My device interface",
)
VLAN.objects.create(vid=1, name="VLAN 1", description="First VLAN")

def test_valid_logical_interface(self):
"""Test that a logical interface can be created."""
vlan = VLAN.objects.get(vid=1)
device_interface = DeviceInterface.objects.get(name="etp1")

logical_interface = LogicalInterface.objects.create(
index=1,
enabled=True,
state="staging",
monitoring_state="disabled",
parent_interface=device_interface,
mtu=1500,
type="l3",
description="My logical interface",
)

logical_interface.tagged_vlans.add(vlan)
logical_interface.save()

def test_invalid_logical_interface_untagged_and_tagged_vlans(self):
"""Test that a logical interface cannot have both tagged and untagged VLANs."""
device_interface = DeviceInterface.objects.get(name="etp1")

# Create VLAN instances
vlan1 = VLAN.objects.create(vid=100, name="VLAN 100", description="First VLAN")
vlan2 = VLAN.objects.create(vid=200, name="VLAN 200", description="Second VLAN")

# Create a working LogicalInterface
logical_interface = LogicalInterface.objects.create(
index=2,
enabled=True,
state="staging",
monitoring_state="disabled",
parent_interface=device_interface,
mtu=1500,
type="l3",
description="My logical interface",
)

# Set an untagged VLAN
logical_interface.untagged_vlan = vlan1
logical_interface.save()

with self.assertRaisesRegex(
ValidationError, "Untagged VLAN cannot be combined with tagged VLANs or native VLAN."
):
# Add a tagged VLAN
logical_interface.tagged_vlans.add(vlan2)
logical_interface.save()

def test_invalid_logical_interface_untagged_and_native_vlans(self):
"""Test that a logical interface cannot have both untagged and native VLANs."""
device_interface = DeviceInterface.objects.get(name="etp1")

vlan1 = VLAN.objects.create(vid=1000, name="VLAN 1000", description="First VLAN")
vlan2 = VLAN.objects.create(vid=2000, name="VLAN 2000", description="Second VLAN")

# Create a working LogicalInterface
logical_interface = LogicalInterface.objects.create(
index=3,
enabled=True,
state="staging",
monitoring_state="disabled",
parent_interface=device_interface,
mtu=1500,
type="l3",
description="My logical interface",
)

# Set an untagged VLAN
logical_interface.untagged_vlan = vlan1
logical_interface.save()

with self.assertRaisesRegex(
ValidationError, "Untagged VLAN cannot be combined with tagged VLANs or native VLAN."
):
# Set a native VLAN
logical_interface.native_vlan = vlan2
logical_interface.save()

0 comments on commit 21c3003

Please sign in to comment.