mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #8222: Enable the assignment of a VM to a specific host device within a cluster
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
# Virtual Machines
|
# Virtual Machines
|
||||||
|
|
||||||
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to exactly one cluster.
|
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to exactly one cluster, and may optionally be assigned to a particular host device within that cluster.
|
||||||
|
|
||||||
Like devices, each VM can be assigned a platform and/or functional role, and must have one of the following operational statuses assigned to it:
|
Like devices, each VM can be assigned a platform and/or functional role, and must have one of the following operational statuses assigned to it:
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
||||||
|
* [#8222](https://github.com/netbox-community/netbox/issues/8222) - Enable the assignment of a VM to a specific host device within a cluster
|
||||||
* [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster
|
* [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster
|
||||||
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
|
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
|
||||||
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
|
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
|
||||||
@ -26,4 +27,6 @@
|
|||||||
* The `nat_inside` field no longer requires a unique value
|
* The `nat_inside` field no longer requires a unique value
|
||||||
* The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses
|
* The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses
|
||||||
* virtualization.Cluster
|
* virtualization.Cluster
|
||||||
* Add required `status` field (default value: `active`)
|
* Added required `status` field (default value: `active`)
|
||||||
|
* virtualization.VirtualMachine
|
||||||
|
* Added `device` field
|
||||||
|
@ -78,9 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Cluster</h5>
|
||||||
Cluster
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -96,13 +94,17 @@
|
|||||||
<th scope="row">Cluster Type</th>
|
<th scope="row">Cluster Type</th>
|
||||||
<td>{{ object.cluster.type }}</td>
|
<td>{{ object.cluster.type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Device</th>
|
||||||
|
<td>
|
||||||
|
{{ object.device|linkify|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Resources</h5>
|
||||||
Resources
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -34,7 +34,7 @@ def post_data(data):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def create_test_device(name):
|
def create_test_device(name, **attrs):
|
||||||
"""
|
"""
|
||||||
Convenience method for creating a Device (e.g. for component testing).
|
Convenience method for creating a Device (e.g. for component testing).
|
||||||
"""
|
"""
|
||||||
@ -42,7 +42,7 @@ def create_test_device(name):
|
|||||||
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer)
|
devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer)
|
||||||
devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1')
|
devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1')
|
||||||
device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole)
|
device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole, **attrs)
|
||||||
|
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import (
|
||||||
|
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
|
||||||
|
)
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
@ -68,6 +70,7 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
|||||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||||
site = NestedSiteSerializer(read_only=True)
|
site = NestedSiteSerializer(read_only=True)
|
||||||
cluster = NestedClusterSerializer()
|
cluster = NestedClusterSerializer()
|
||||||
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
@ -78,9 +81,9 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip',
|
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||||
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@ -90,9 +93,9 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
|
|
||||||
class Meta(VirtualMachineSerializer.Meta):
|
class Meta(VirtualMachineSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip',
|
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||||
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data',
|
||||||
'custom_fields', 'config_context', 'created', 'last_updated',
|
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
@ -54,7 +54,7 @@ class ClusterViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||||
queryset = VirtualMachine.objects.prefetch_related(
|
queryset = VirtualMachine.objects.prefetch_related(
|
||||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
'cluster__site', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||||
)
|
)
|
||||||
filterset_class = filtersets.VirtualMachineFilterSet
|
filterset_class = filtersets.VirtualMachineFilterSet
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
@ -150,6 +150,16 @@ class VirtualMachineFilterSet(
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Cluster',
|
label='Cluster',
|
||||||
)
|
)
|
||||||
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__name',
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Device',
|
||||||
|
)
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='cluster__site__region',
|
field_name='cluster__site__region',
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from netbox.forms import NetBoxModelBulkEditForm
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -110,6 +110,13 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'cluster_id': '$cluster'
|
||||||
|
}
|
||||||
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(
|
queryset=DeviceRole.objects.filter(
|
||||||
vm_role=True
|
vm_role=True
|
||||||
@ -146,11 +153,11 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('cluster', 'status', 'role', 'tenant', 'platform')),
|
(None, ('cluster', 'device', 'status', 'role', 'tenant', 'platform')),
|
||||||
('Resources', ('vcpus', 'memory', 'disk'))
|
('Resources', ('vcpus', 'memory', 'disk'))
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Platform, Site
|
from dcim.models import Device, DeviceRole, Platform, Site
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelCSVForm
|
from netbox.forms import NetBoxModelCSVForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -76,6 +76,12 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text='Assigned cluster'
|
help_text='Assigned cluster'
|
||||||
)
|
)
|
||||||
|
device = CSVModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Assigned device within cluster'
|
||||||
|
)
|
||||||
role = CSVModelChoiceField(
|
role = CSVModelChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(
|
queryset=DeviceRole.objects.filter(
|
||||||
vm_role=True
|
vm_role=True
|
||||||
@ -100,7 +106,7 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'name', 'status', 'role', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
@ -87,7 +87,7 @@ class VirtualMachineFilterForm(
|
|||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')),
|
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -110,6 +110,11 @@ class VirtualMachineFilterForm(
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Cluster')
|
label=_('Cluster')
|
||||||
)
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device')
|
||||||
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -179,6 +179,13 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
'group_id': '$cluster_group'
|
'group_id': '$cluster_group'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'cluster_id': '$cluster'
|
||||||
|
}
|
||||||
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -197,7 +204,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
||||||
('Cluster', ('cluster_group', 'cluster')),
|
('Cluster', ('cluster_group', 'cluster', 'device')),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
||||||
('Resources', ('vcpus', 'memory', 'disk')),
|
('Resources', ('vcpus', 'memory', 'disk')),
|
||||||
@ -207,8 +214,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
|
'name', 'status', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform',
|
||||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
|
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
|
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.0.4 on 2022-05-25 19:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0153_created_datetimefield'),
|
||||||
|
('virtualization', '0030_cluster_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='device',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.device'),
|
||||||
|
),
|
||||||
|
]
|
@ -200,6 +200,13 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='virtual_machines'
|
related_name='virtual_machines'
|
||||||
)
|
)
|
||||||
|
device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_machines',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -316,6 +323,12 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
# Validate assigned cluster device
|
||||||
|
if self.device and self.device not in self.cluster.devices.all():
|
||||||
|
raise ValidationError({
|
||||||
|
'device': f'The selected device ({self.device} is not assigned to this cluster ({self.cluster}).'
|
||||||
|
})
|
||||||
|
|
||||||
# Validate primary IP addresses
|
# Validate primary IP addresses
|
||||||
interfaces = self.interfaces.all()
|
interfaces = self.interfaces.all()
|
||||||
for field in ['primary_ip4', 'primary_ip6']:
|
for field in ['primary_ip4', 'primary_ip6']:
|
||||||
|
@ -33,6 +33,9 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
cluster = tables.Column(
|
cluster = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
device = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
role = columns.ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
@ -56,7 +59,7 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
'pk', 'id', 'name', 'status', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||||
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
@ -152,8 +152,15 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
|
device1 = create_test_device('device1')
|
||||||
|
device1.cluster = clusters[0]
|
||||||
|
device1.save()
|
||||||
|
device2 = create_test_device('device2')
|
||||||
|
device2.cluster = clusters[1]
|
||||||
|
device2.save()
|
||||||
|
|
||||||
virtual_machines = (
|
virtual_machines = (
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], local_context_data={'A': 1}),
|
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], device=device1, local_context_data={'A': 1}),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], local_context_data={'B': 2}),
|
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], local_context_data={'B': 2}),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], local_context_data={'C': 3}),
|
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], local_context_data={'C': 3}),
|
||||||
)
|
)
|
||||||
@ -163,6 +170,7 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
{
|
{
|
||||||
'name': 'Virtual Machine 4',
|
'name': 'Virtual Machine 4',
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': device2.pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Virtual Machine 5',
|
'name': 'Virtual Machine 5',
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from ipam.models import IPAddress, VRF
|
from ipam.models import IPAddress, VRF
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.filtersets import *
|
from virtualization.filtersets import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -225,9 +225,9 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
site_group.save()
|
site_group.save()
|
||||||
|
|
||||||
sites = (
|
sites = (
|
||||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
Site(name='Site 1', slug='site-1', region=regions[0], group=site_groups[0]),
|
||||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
Site(name='Site 2', slug='site-2', region=regions[1], group=site_groups[1]),
|
||||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=site_groups[2]),
|
||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
@ -252,6 +252,12 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
DeviceRole.objects.bulk_create(roles)
|
DeviceRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('device1', cluster=clusters[0]),
|
||||||
|
create_test_device('device2', cluster=clusters[1]),
|
||||||
|
create_test_device('device3', cluster=clusters[2]),
|
||||||
|
)
|
||||||
|
|
||||||
tenant_groups = (
|
tenant_groups = (
|
||||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||||
@ -268,9 +274,9 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
vms = (
|
vms = (
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], tenant=tenants[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], device=devices[0], platform=platforms[0], role=roles[0], tenant=tenants[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], tenant=tenants[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], device=devices[1], platform=platforms[1], role=roles[1], tenant=tenants[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], tenant=tenants[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], device=devices[2], platform=platforms[2], role=roles[2], tenant=tenants[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
@ -331,6 +337,13 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'cluster': [clusters[0].name, clusters[1].name]}
|
params = {'cluster': [clusters[0].name, clusters[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_device(self):
|
||||||
|
devices = Device.objects.all()[:2]
|
||||||
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
@ -5,7 +5,7 @@ from netaddr import EUI
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Platform, Site
|
from dcim.models import DeviceRole, Platform, Site
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from utilities.testing import ViewTestCases, create_tags
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
@ -176,16 +176,22 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('device1', cluster=clusters[0]),
|
||||||
|
create_test_device('device2', cluster=clusters[1]),
|
||||||
|
)
|
||||||
|
|
||||||
VirtualMachine.objects.bulk_create([
|
VirtualMachine.objects.bulk_create([
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': devices[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'platform': platforms[1].pk,
|
'platform': platforms[1].pk,
|
||||||
'name': 'Virtual Machine X',
|
'name': 'Virtual Machine X',
|
||||||
@ -202,14 +208,15 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,status,cluster",
|
"name,status,cluster,device",
|
||||||
"Virtual Machine 4,active,Cluster 1",
|
"Virtual Machine 4,active,Cluster 1,device1",
|
||||||
"Virtual Machine 5,active,Cluster 1",
|
"Virtual Machine 5,active,Cluster 1,device1",
|
||||||
"Virtual Machine 6,active,Cluster 1",
|
"Virtual Machine 6,active,Cluster 1,",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': devices[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'platform': platforms[1].pk,
|
'platform': platforms[1].pk,
|
||||||
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
||||||
|
Reference in New Issue
Block a user