Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Upload Python Package

on:
release:
types: [published]

permissions:
contents: read

jobs:
release-build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build

- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/

pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write

# Dedicated environments with protections for publishing are strongly recommended.
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
# url: https://pypi.org/p/YOURPROJECT
#
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
# ALTERNATIVE: exactly, uncomment the following line instead:
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}

steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/

- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
.vscode/
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following functionality is currently offered by this plugin:
* The ability to view/create/update/delete *Multi-Chassis Link Aggregation Group* objects through the web UI and by API.
* Button added to Device view to "Show MC Domain" if a device is associated with an MC Domain
* Button added to Interface view to "Show MC-LAG" if an interface is associated with an MC-LAG
* Extra Column added in the Interface Table to see the related MC-LAG Domain and its Group

## Configuration template example

Expand Down Expand Up @@ -85,7 +86,7 @@ exit

## Supported versions

The current version of the `netbox-plugin-mclag` plugin has been developed for Netbox 4.2. and is not supported on older versions.
Version 0.3 and newer of the `netbox-plugin-mclag` plugin have added compatibility for NetBox 4.3 and should have backwards compatibility with NetBox 4.2. It is not supported on older versions.

The plugin has been developed for Python 3.12.3, but I expect it to work with any Python version supported by Netbox.

Expand Down Expand Up @@ -139,6 +140,20 @@ PLUGINS_CONFIG = {
}
```

The default list of 'LAG' interface types is limited. This can be extended using the FIELD_CHOICES in the configuration.py:
See https://netbox.readthedocs.io/en/feature/configuration/data-validation/#field_choices

```python
FIELD_CHOICES = {
'netbox_plugin_mclag.McLag.type+': (
('value', 'Display', 'color'),
)
}
```
Remark:
- Remove the + after 'netbox_plugin_mclag.McLag.type' to replace the default list, keep the + to add to the list.
- Make sure to have a comma after the last entry !

## Uninstallation

If you decide the plugin isn't for you and you want to remove it, this is how.
Expand Down
5 changes: 4 additions & 1 deletion netbox_plugin_mclag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ class NetBoxMcLagConfig(PluginConfig):
name = "netbox_plugin_mclag"
verbose_name = "Multi-Chassis LAG"
description = "Manage Multi-Chassis Link Aggregation Groups in Netbox (MC-LAG / MLAG / vPC / etc)"
version = "0.2.0"
author = "Pieter Lambrecht"
author_email = "pieter.lambrecht@gmail.com"
version = "0.3.0"
base_url = "mclag"

# based original code of pv2b: https://github.com/pv2b/netbox-plugin-mclag

config = NetBoxMcLagConfig
17 changes: 11 additions & 6 deletions netbox_plugin_mclag/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from dcim.api.serializers import DeviceSerializer, InterfaceSerializer
from ..models import McLag, McDomain
from dcim.models import Interface
from ..util import get_interface_label

from netbox_plugin_mclag.models import McLag, McDomain
from netbox_plugin_mclag.util import get_interface_label

#
# Nested serializers
Expand All @@ -15,21 +16,23 @@ class NestedMcDomainSerializer(WritableNestedSerializer):
view_name='plugins-api:netbox_plugin_mclag-api:mcdomain-detail'
)
devices = DeviceSerializer(nested=True, many=True)
display = serializers.SerializerMethodField(read_only=True)

class Meta:
model = McDomain
fields = ('id', 'url', 'name', 'domain_id', 'devices')
fields = ('id', 'url', 'name', 'display', 'domain_id', 'devices')


class NestedMcLagSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_plugin_mclag-api:mclag-detail'
)
interfaces = InterfaceSerializer(nested=True, many=True)
display = serializers.SerializerMethodField(read_only=True)

class Meta:
model = McLag
fields = ('id', 'name', 'url', 'name', 'lag_id', 'interfaces')
fields = ('id', 'name', 'display', 'url', 'name', 'lag_id', 'type', 'interfaces')

#
# Regular serializers
Expand All @@ -41,11 +44,12 @@ class McDomainSerializer(NetBoxModelSerializer):
)
mc_lags = NestedMcLagSerializer(many=True)
devices = DeviceSerializer(nested=True, many=True)
display = serializers.SerializerMethodField(read_only=True)

class Meta:
model = McDomain
fields = (
'id', 'url', 'name', 'domain_id', 'devices', 'description', 'mc_lags', 'tags', 'custom_fields',
'id', 'url', 'name', 'display', 'domain_id', 'devices', 'description', 'mc_lags', 'tags', 'custom_fields',
'created', 'last_updated',
)

Expand All @@ -56,11 +60,12 @@ class McLagSerializer(NetBoxModelSerializer):
)
mc_domain = NestedMcDomainSerializer()
interfaces = InterfaceSerializer(nested=True, many=True)
display = serializers.SerializerMethodField(read_only=True)

class Meta:
model = McLag
fields = (
'id', 'url', 'name', 'lag_id', 'description', 'mc_domain', 'interfaces', 'tags', 'custom_fields',
'id', 'url', 'name', 'display', 'lag_id', 'type', 'description', 'mc_domain', 'interfaces', 'tags', 'custom_fields',
'created', 'last_updated',
)

Expand Down
2 changes: 1 addition & 1 deletion netbox_plugin_mclag/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from netbox.api.routers import NetBoxRouter
from . import views

from netbox_plugin_mclag.api import views

app_name = 'netbox_plugin_mclag'

Expand Down
14 changes: 10 additions & 4 deletions netbox_plugin_mclag/api/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from dcim.models import Interface
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet

from .. import models
from .serializers import McLagSerializer, McDomainSerializer, McInterfaceSerializer
from ..filtersets import McInterfaceFilterSet
from dcim.models import Interface
from netbox_plugin_mclag import models
from netbox_plugin_mclag.api.serializers import McLagSerializer, McDomainSerializer, McInterfaceSerializer
from netbox_plugin_mclag.filtersets import McInterfaceFilterSet

class McDomainViewSet(NetBoxModelViewSet):
queryset = models.McDomain.objects.prefetch_related('devices', 'tags')
serializer_class = McDomainSerializer
ordering_fields = ['name', 'domain_id']
ordering = ['name']

class McLagViewSet(NetBoxModelViewSet):
queryset = models.McLag.objects.prefetch_related('mc_domain', 'tags')
serializer_class = McLagSerializer
ordering_fields = ['lag_id', 'name', 'type']
ordering = ['lag_id', 'name']

class McInterfaceViewSet(NetBoxReadOnlyModelViewSet):
# Force disabling of brief mode that is implemented in the BriefModeMixin.
Expand All @@ -26,3 +30,5 @@ def initialize_request(self, request, *args, **kwargs):
queryset = Interface.objects.filter(type='lag').prefetch_related('device')
serializer_class = McInterfaceSerializer
filterset_class = McInterfaceFilterSet
ordering_fields = ['name', 'device__name']
ordering = ['name']
21 changes: 21 additions & 0 deletions netbox_plugin_mclag/choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from utilities.choices import ChoiceSet

class LagTypeChoices(ChoiceSet):
key = 'McLag.type'

# a data channel part of the MC-LAG
LAGTYPE_CHANNEL = 'channel'

# control and management protocol channes of the MC-LAG
LAGTYPE_ICCP = 'iccp'
LAGTYPE_MCLAG = 'mclag'
LAGTYPE_PEERLINK = 'peerlink'
LAGTYPE_PEERKEEPALIVE = 'peerkeepalive'

CHOICES = [
(LAGTYPE_CHANNEL, 'Channel', 'green'),
(LAGTYPE_ICCP, 'ICCP', 'blue'),
(LAGTYPE_MCLAG, 'MC-LAG', 'blue'),
(LAGTYPE_PEERLINK, 'Peer-Link', 'blue'),
(LAGTYPE_PEERKEEPALIVE, 'Peer-Keepalive', 'blue'),
]
7 changes: 4 additions & 3 deletions netbox_plugin_mclag/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from netbox.filtersets import NetBoxModelFilterSet
from dcim.models import Interface
from .models import McDomain, McLag
from django_filters import ModelMultipleChoiceFilter
from dcim.models import Interface
from netbox.filtersets import NetBoxModelFilterSet

from netbox_plugin_mclag.models import McDomain, McLag

class McInterfaceFilterSet(NetBoxModelFilterSet):
mc_domain = ModelMultipleChoiceFilter(
Expand Down
62 changes: 55 additions & 7 deletions netbox_plugin_mclag/forms.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
from django import forms

from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelMultipleChoiceField
from dcim.models import Interface, Device
from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, DynamicModelChoiceField, CommentField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelectMultiple
from dcim.models import Interface
from .models import McDomain, McLag
from django.db.models.functions import Concat
from .util import get_interface_label

from netbox_plugin_mclag.models import McDomain, McLag
from netbox_plugin_mclag.util import get_interface_label

class McDomainForm(NetBoxModelForm):
class Meta:
model = McDomain
fields = ('name', 'domain_id', 'description', 'devices', 'tags')

class McDomainBulkEditForm(NetBoxModelBulkEditForm):
domain_id = forms.CharField(
required=False,
)
devices = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
)
description = forms.CharField(
required=False,
widget=forms.Textarea,
)

model = McDomain
nullable_fields = ('description', 'tags',)
fields = ('domain_id', 'devices', 'description', 'tags')
field_order = ('domain_id', 'devices', 'description', 'tags')
fieldsets = (
FieldSet('domain_id', 'devices', 'description', name='Multi-Chassis Domain Group'),
)

class McInterfaceMultipleChoiceField(DynamicModelMultipleChoiceField):
def label_from_instance(self, interface):
return get_interface_label(interface)
Expand All @@ -31,4 +53,30 @@ class McLagForm(NetBoxModelForm):
)
class Meta:
model = McLag
fields = ('name', 'lag_id', 'description', 'mc_domain', 'tags', 'interfaces')
fields = ('name', 'mc_domain', 'type', 'lag_id', 'interfaces', 'description', 'tags')

class McLagBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
choices=[('', '---------'),] + McLag._meta.get_field('type').choices,
required=False,
initial=''
)
lag_id = forms.CharField(
required=False,
)
mc_domain = DynamicModelChoiceField(
queryset=McDomain.objects.all(),
required=False,
)
description = forms.CharField(
required=False,
widget=forms.Textarea,
)

model = McLag
nullable_fields = ('description', 'tags',)
fields = ('name', 'mc_domain', 'type', 'lag_id', 'interfaces', 'description', 'tags')
field_order = ('mc_domain', 'type', 'lag_id', 'description', 'tags')
fieldsets = (
FieldSet('type', 'mc_domain', 'description', name='Multi-Chassis Link Aggregation Group'),
)
18 changes: 18 additions & 0 deletions netbox_plugin_mclag/migrations/0003_mclag_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.5 on 2025-09-23 07:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('netbox_plugin_mclag', '0002_alter_mcdomain_options_alter_mclag_options_and_more'),
]

operations = [
migrations.AddField(
model_name='mclag',
name='type',
field=models.CharField(default='channel', max_length=20),
),
]
Loading