Skip to content

Commit c8ace64

Browse files
committed
Fixes #55: Add VirtualMachine support for License and Contract assignments
- Add virtual_machine ForeignKey to LicenseAssignment and SupportContractAssignment models - Enforce mutual exclusivity between device and virtual_machine via clean() and CheckConstraint - Module assignments restricted to devices only (not VMs) - Update forms with TabbedGroups for Device vs Virtual Machine selection - Add virtual_machine filters to filtersets - Add virtual_machine to API serializers with nested VirtualMachineSerializer - Add virtual_machine columns to tables - Add Support Contracts card to VirtualMachine detail pages via template extension - Add HTMX views for lazy-loading VM contract data - Add comprehensive tests for VM functionality - Update README to document VM support
1 parent de206a2 commit c8ace64

18 files changed

Lines changed: 802 additions & 54 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ The Netbox Lifecycle plugin is a Hardware EOS/EOL, License and Support Contract
44

55
## Features
66

7-
* Tracking EOL/EOS data
8-
* Tracking License
9-
* Tracking Support Contracts
7+
* Tracking EOL/EOS data for DeviceTypes and ModuleTypes
8+
* Tracking Licenses (assignable to Devices and Virtual Machines)
9+
* Tracking Support Contracts (assignable to Devices, Modules, and Virtual Machines)
1010

1111
# Requirements
1212

@@ -49,15 +49,15 @@ PLUGINS_CONFIG = {
4949
| Setting | Default | Description |
5050
|---------|---------|-------------|
5151
| `lifecycle_card_position` | `right_page` | Position of the Hardware Lifecycle Info card on Device, Module, DeviceType, and ModuleType detail pages. Options: `left_page`, `right_page`, `full_width_page`. |
52-
| `contract_card_position` | `right_page` | Position of the Support Contracts card on Device detail pages. Options: `left_page`, `right_page`, `full_width_page`. |
52+
| `contract_card_position` | `right_page` | Position of the Support Contracts card on Device and VirtualMachine detail pages. Options: `left_page`, `right_page`, `full_width_page`. |
5353

5454
### Hardware Lifecycle Info Card
5555

5656
Displays EOL/EOS information for the hardware type on Device, Module, DeviceType, and ModuleType detail pages.
5757

5858
### Support Contracts Card
5959

60-
Displays all contract assignments on Device detail pages, grouped by status:
60+
Displays all contract assignments on Device and VirtualMachine detail pages, grouped by status:
6161

6262
- **Active**: Contracts currently in effect
6363
- **Future**: Contracts with a start date in the future

netbox_lifecycle/api/_serializers/contract.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dcim.api.serializers_.devices import DeviceSerializer, ModuleSerializer
44
from dcim.api.serializers_.manufacturers import ManufacturerSerializer
55
from netbox.api.serializers import NetBoxModelSerializer
6+
from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer
67
from netbox_lifecycle.api._serializers.license import LicenseAssignmentSerializer
78
from netbox_lifecycle.api._serializers.vendor import VendorSerializer
89
from netbox_lifecycle.models import (
@@ -88,6 +89,9 @@ class SupportContractAssignmentSerializer(NetBoxModelSerializer):
8889
sku = SupportSKUSerializer(nested=True, required=False, allow_null=True)
8990
device = DeviceSerializer(nested=True, required=False, allow_null=True)
9091
module = ModuleSerializer(nested=True, required=False, allow_null=True)
92+
virtual_machine = VirtualMachineSerializer(
93+
nested=True, required=False, allow_null=True
94+
)
9195
license = LicenseAssignmentSerializer(nested=True, required=False, allow_null=True)
9296

9397
class Meta:
@@ -100,6 +104,7 @@ class Meta:
100104
'sku',
101105
'device',
102106
'module',
107+
'virtual_machine',
103108
'license',
104109
'end',
105110
'description',
@@ -116,5 +121,6 @@ class Meta:
116121
'sku',
117122
'device',
118123
'module',
124+
'virtual_machine',
119125
'license',
120126
)

netbox_lifecycle/api/_serializers/license.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dcim.api.serializers_.devices import DeviceSerializer
44
from dcim.api.serializers_.manufacturers import ManufacturerSerializer
55
from netbox.api.serializers import NetBoxModelSerializer
6+
from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer
67
from netbox_lifecycle.api._serializers.vendor import VendorSerializer
78
from netbox_lifecycle.models import License, LicenseAssignment
89

@@ -46,6 +47,9 @@ class LicenseAssignmentSerializer(NetBoxModelSerializer):
4647
license = LicenseSerializer(nested=True)
4748
vendor = VendorSerializer(nested=True)
4849
device = DeviceSerializer(nested=True, required=False, allow_null=True)
50+
virtual_machine = VirtualMachineSerializer(
51+
nested=True, required=False, allow_null=True
52+
)
4953

5054
class Meta:
5155
model = LicenseAssignment
@@ -56,6 +60,8 @@ class Meta:
5660
'vendor',
5761
'license',
5862
'device',
63+
'virtual_machine',
64+
'quantity',
5965
'description',
6066
'comments',
6167
'tags',
@@ -68,4 +74,5 @@ class Meta:
6874
'vendor',
6975
'license',
7076
'device',
77+
'virtual_machine',
7178
)

netbox_lifecycle/filtersets/contract.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from dcim.models import Manufacturer, Device, Module
66
from netbox.filtersets import NetBoxModelFilterSet
7+
from virtualization.models import VirtualMachine
78
from netbox_lifecycle.models import (
89
Vendor,
910
SupportContract,
@@ -149,6 +150,17 @@ class SupportContractAssignmentFilterSet(NetBoxModelFilterSet):
149150
to_field_name='name',
150151
label=_('License (SKU)'),
151152
)
153+
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
154+
field_name='virtual_machine',
155+
queryset=VirtualMachine.objects.all(),
156+
label=_('Virtual Machine (ID)'),
157+
)
158+
virtual_machine = django_filters.ModelMultipleChoiceFilter(
159+
field_name='virtual_machine__name',
160+
queryset=VirtualMachine.objects.all(),
161+
to_field_name='name',
162+
label=_('Virtual Machine (name)'),
163+
)
152164
device_status = django_filters.ModelMultipleChoiceFilter(
153165
field_name='device__status',
154166
queryset=Device.objects.all(),
@@ -173,7 +185,9 @@ def search(self, queryset, name, value):
173185
| Q(device__name__icontains=value)
174186
| Q(module__serial__icontains=value)
175187
| Q(module__module_type__model__icontains=value)
188+
| Q(virtual_machine__name__icontains=value)
176189
| Q(license__device__name__icontains=value)
190+
| Q(license__virtual_machine__name__icontains=value)
177191
| Q(license__license__name__icontains=value)
178192
)
179193
return queryset.filter(qs_filter).distinct()

netbox_lifecycle/filtersets/license.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from dcim.models import Manufacturer, Device
66
from netbox.filtersets import NetBoxModelFilterSet
7+
from virtualization.models import VirtualMachine
78
from netbox_lifecycle.models import Vendor, License, LicenseAssignment
89

910
__all__ = (
@@ -74,6 +75,17 @@ class LicenseAssignmentFilterSet(NetBoxModelFilterSet):
7475
to_field_name='name',
7576
label=_('Device'),
7677
)
78+
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
79+
field_name='virtual_machine',
80+
queryset=VirtualMachine.objects.all(),
81+
label=_('Virtual Machine'),
82+
)
83+
virtual_machine = django_filters.ModelMultipleChoiceFilter(
84+
field_name='virtual_machine__name',
85+
queryset=VirtualMachine.objects.all(),
86+
to_field_name='name',
87+
label=_('Virtual Machine'),
88+
)
7789

7890
class Meta:
7991
model = LicenseAssignment
@@ -90,5 +102,6 @@ def search(self, queryset, name, value):
90102
| Q(license__name__icontains=value)
91103
| Q(vendor__name__icontains=value)
92104
| Q(device__name__icontains=value)
105+
| Q(virtual_machine__name__icontains=value)
93106
)
94107
return queryset.filter(qs_filter).distinct()

netbox_lifecycle/forms/filtersets.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dcim.choices import DeviceStatusChoices
88
from dcim.models import Device, Manufacturer, Module
99
from netbox.forms import NetBoxModelFilterSetForm
10+
from virtualization.models import VirtualMachine
1011
from netbox_lifecycle.models import (
1112
HardwareLifecycle,
1213
SupportContract,
@@ -138,6 +139,7 @@ class SupportContractAssignmentFilterForm(NetBoxModelFilterSetForm):
138139
'contract_id',
139140
'device_id',
140141
'module_id',
142+
'virtual_machine_id',
141143
'license_id',
142144
'device_status',
143145
name='Assignment',
@@ -166,6 +168,12 @@ class SupportContractAssignmentFilterForm(NetBoxModelFilterSetForm):
166168
required=False,
167169
label=_('Module'),
168170
)
171+
virtual_machine_id = DynamicModelMultipleChoiceField(
172+
queryset=VirtualMachine.objects.all(),
173+
required=False,
174+
selector=True,
175+
label=_('Virtual Machines'),
176+
)
169177
device_status = forms.MultipleChoiceField(
170178
label=_('Status'), choices=DeviceStatusChoices, required=False
171179
)
@@ -176,7 +184,13 @@ class LicenseAssignmentFilterForm(NetBoxModelFilterSetForm):
176184
model = LicenseAssignment
177185
fieldsets = (
178186
FieldSet('q', 'filter_id', 'tag'),
179-
FieldSet('license_id', 'vendor_id', 'device_id', name='Assignment'),
187+
FieldSet(
188+
'license_id',
189+
'vendor_id',
190+
'device_id',
191+
'virtual_machine_id',
192+
name='Assignment',
193+
),
180194
)
181195
license_id = DynamicModelMultipleChoiceField(
182196
queryset=License.objects.all(),
@@ -196,4 +210,10 @@ class LicenseAssignmentFilterForm(NetBoxModelFilterSetForm):
196210
selector=True,
197211
label=_('Devices'),
198212
)
213+
virtual_machine_id = DynamicModelMultipleChoiceField(
214+
queryset=VirtualMachine.objects.all(),
215+
required=False,
216+
selector=True,
217+
label=_('Virtual Machines'),
218+
)
199219
tag = TagFilterField(model)

0 commit comments

Comments
 (0)