1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #5303: A virtual machine may be assigned to a site and/or cluster

This commit is contained in:
jeremystretch
2022-05-26 14:59:49 -04:00
parent b331f047af
commit db42589cca
19 changed files with 223 additions and 86 deletions

View File

@ -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, and may optionally be assigned to a particular host device within that cluster. A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to a site and/or cluster, and may optionally be assigned to a particular host device within a 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:

View File

@ -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
* [#5303](https://github.com/netbox-community/netbox/issues/5303) - A virtual machine may be assigned to a site and/or cluster
* [#8222](https://github.com/netbox-community/netbox/issues/8222) - Enable the assignment of a VM to a specific host device within a cluster * [#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
@ -30,3 +31,5 @@
* Added required `status` field (default value: `active`) * Added required `status` field (default value: `active`)
* virtualization.VirtualMachine * virtualization.VirtualMachine
* Added `device` field * Added `device` field
* The `site` field is now directly writable (rather than being inferred from the assigned cluster)
* The `cluster` field is now optional. A virtual machine must have a site and/or cluster assigned.

View File

@ -81,13 +81,19 @@
<h5 class="card-header">Cluster</h5> <h5 class="card-header">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>
<th scope="row">Site</th>
<td>
{{ object.site|linkify|placeholder }}
</td>
</tr>
<tr> <tr>
<th scope="row">Cluster</th> <th scope="row">Cluster</th>
<td> <td>
{% if object.cluster.group %} {% if object.cluster.group %}
{{ object.cluster.group|linkify }} / {{ object.cluster.group|linkify }} /
{% endif %} {% endif %}
{{ object.cluster|linkify }} {{ object.cluster|linkify|placeholder }}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -34,10 +34,11 @@ def post_data(data):
return ret return ret
def create_test_device(name, **attrs): def create_test_device(name, site=None, **attrs):
""" """
Convenience method for creating a Device (e.g. for component testing). Convenience method for creating a Device (e.g. for component testing).
""" """
if site is None:
site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1') site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1')
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)

View File

@ -68,8 +68,8 @@ class ClusterSerializer(NetBoxModelSerializer):
class VirtualMachineSerializer(NetBoxModelSerializer): class VirtualMachineSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
site = NestedSiteSerializer(read_only=True) site = NestedSiteSerializer(required=False, allow_null=True)
cluster = NestedClusterSerializer() cluster = NestedClusterSerializer(required=False, allow_null=True)
device = NestedDeviceSerializer(required=False, allow_null=True) 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)

View File

@ -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', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags' 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
) )
filterset_class = filtersets.VirtualMachineFilterSet filterset_class = filtersets.VirtualMachineFilterSet

View File

@ -162,37 +162,36 @@ class VirtualMachineFilterSet(
) )
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='cluster__site__region', field_name='site__region',
lookup_expr='in', lookup_expr='in',
label='Region (ID)', label='Region (ID)',
) )
region = TreeNodeMultipleChoiceFilter( region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='cluster__site__region', field_name='site__region',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter( site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
field_name='cluster__site__group', field_name='site__group',
lookup_expr='in', lookup_expr='in',
label='Site group (ID)', label='Site group (ID)',
) )
site_group = TreeNodeMultipleChoiceFilter( site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
field_name='cluster__site__group', field_name='site__group',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Site group (slug)', label='Site group (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='cluster__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = django_filters.ModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
field_name='cluster__site__slug', field_name='site__slug',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',

View File

@ -106,9 +106,16 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
initial='', initial='',
widget=StaticSelect(), widget=StaticSelect(),
) )
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
)
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False required=False,
query_params={
'site_id': '$site'
}
) )
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -153,11 +160,11 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
model = VirtualMachine model = VirtualMachine
fieldsets = ( fieldsets = (
(None, ('cluster', 'device', 'status', 'role', 'tenant', 'platform')), (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform')),
('Resources', ('vcpus', 'memory', 'disk')) ('Resources', ('vcpus', 'memory', 'disk'))
) )
nullable_fields = ( nullable_fields = (
'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
) )
@ -236,8 +243,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
# See 5643 # See 5643
if 'pk' in self.initial: if 'pk' in self.initial:
site = None site = None
interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related( interfaces = VMInterface.objects.filter(
'virtual_machine__cluster__site' pk__in=self.initial['pk']
).prefetch_related(
'virtual_machine__site'
) )
# Check interface sites. First interface should set site, further interfaces will either continue the # Check interface sites. First interface should set site, further interfaces will either continue the

View File

@ -71,9 +71,16 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
help_text='Operational status' help_text='Operational status'
) )
site = CSVModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
required=False,
help_text='Assigned site'
)
cluster = CSVModelChoiceField( cluster = CSVModelChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
to_field_name='name', to_field_name='name',
required=False,
help_text='Assigned cluster' help_text='Assigned cluster'
) )
device = CSVModelChoiceField( device = CSVModelChoiceField(
@ -106,7 +113,8 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'name', 'status', 'role', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', 'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
'comments',
) )

View File

@ -165,6 +165,9 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
class VirtualMachineForm(TenancyForm, NetBoxModelForm): class VirtualMachineForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all()
)
cluster_group = DynamicModelChoiceField( cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False, required=False,
@ -176,7 +179,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
query_params={ query_params={
'group_id': '$cluster_group' 'site_id': '$site',
'group_id': '$cluster_group',
} }
) )
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
@ -204,7 +208,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
fieldsets = ( fieldsets = (
('Virtual Machine', ('name', 'role', 'status', 'tags')), ('Virtual Machine', ('name', 'role', 'status', 'tags')),
('Cluster', ('cluster_group', 'cluster', 'device')), ('Cluster', ('site', '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')),
@ -214,8 +218,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = [ fields = [
'name', 'status', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', 'platform', '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 "

View File

@ -1,20 +0,0 @@
# 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'),
),
]

View File

@ -0,0 +1,28 @@
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='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.site'),
),
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'),
),
migrations.AlterField(
model_name='virtualmachine',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.cluster'),
),
]

View File

@ -0,0 +1,27 @@
from django.db import migrations
def update_virtualmachines_site(apps, schema_editor):
"""
Automatically set the site for all virtual machines.
"""
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
virtual_machines = VirtualMachine.objects.filter(cluster__site__isnull=False)
for vm in virtual_machines:
vm.site = vm.cluster.site
VirtualMachine.objects.bulk_update(virtual_machines, ['site'])
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0031_virtualmachine_site_device'),
]
operations = [
migrations.RunPython(
code=update_virtualmachines_site,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -195,10 +195,19 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
""" """
A virtual machine which runs inside a Cluster. A virtual machine which runs inside a Cluster.
""" """
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='virtual_machines',
blank=True,
null=True
)
cluster = models.ForeignKey( cluster = models.ForeignKey(
to='virtualization.Cluster', to='virtualization.Cluster',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='virtual_machines' related_name='virtual_machines',
blank=True,
null=True
) )
device = models.ForeignKey( device = models.ForeignKey(
to='dcim.Device', to='dcim.Device',
@ -291,7 +300,7 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
objects = ConfigContextModelQuerySet.as_manager() objects = ConfigContextModelQuerySet.as_manager()
clone_fields = [ clone_fields = [
'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk', 'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
] ]
class Meta: class Meta:
@ -323,6 +332,22 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
def clean(self): def clean(self):
super().clean() super().clean()
# Must be assigned to a site and/or cluster
if not self.site and not self.cluster:
raise ValidationError({
'cluster': f'A virtual machine must be assigned to a site and/or cluster.'
})
# Validate site for cluster & device
if self.cluster and self.cluster.site != self.site:
raise ValidationError({
'cluster': f'The selected cluster ({self.cluster} is not assigned to this site ({self.site}).'
})
if self.device and self.device.site != self.site:
raise ValidationError({
'device': f'The selected device ({self.device} is not assigned to this site ({self.site}).'
})
# Validate assigned cluster device # Validate assigned cluster device
if self.device and self.device not in self.cluster.devices.all(): if self.device and self.device not in self.cluster.devices.all():
raise ValidationError({ raise ValidationError({
@ -357,10 +382,6 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
else: else:
return None return None
@property
def site(self):
return self.cluster.site
# #
# Interfaces # Interfaces

View File

@ -30,6 +30,9 @@ class VirtualMachineTable(NetBoxTable):
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
site = tables.Column(
linkify=True
)
cluster = tables.Column( cluster = tables.Column(
linkify=True linkify=True
) )
@ -59,11 +62,11 @@ class VirtualMachineTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'id', 'name', 'status', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory',
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', 'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
) )

View File

@ -2,6 +2,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.models import Site
from ipam.models import VLAN, VRF from ipam.models import VLAN, VRF
from utilities.testing import APITestCase, APIViewTestCases, create_test_device from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from virtualization.choices import * from virtualization.choices import *
@ -146,39 +147,49 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1') clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
)
Site.objects.bulk_create(sites)
clusters = ( clusters = (
Cluster(name='Cluster 1', type=clustertype, group=clustergroup), Cluster(name='Cluster 1', type=clustertype, site=sites[0], group=clustergroup),
Cluster(name='Cluster 2', type=clustertype, group=clustergroup), Cluster(name='Cluster 2', type=clustertype, site=sites[1], group=clustergroup),
Cluster(name='Cluster 3', type=clustertype),
) )
Cluster.objects.bulk_create(clusters) Cluster.objects.bulk_create(clusters)
device1 = create_test_device('device1') device1 = create_test_device('device1', site=sites[0], cluster=clusters[0])
device1.cluster = clusters[0] device2 = create_test_device('device2', site=sites[1], cluster=clusters[1])
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], device=device1, local_context_data={'A': 1}), VirtualMachine(name='Virtual Machine 1', site=sites[0], 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', site=sites[0], 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', site=sites[0], cluster=clusters[0], local_context_data={'C': 3}),
) )
VirtualMachine.objects.bulk_create(virtual_machines) VirtualMachine.objects.bulk_create(virtual_machines)
cls.create_data = [ cls.create_data = [
{ {
'name': 'Virtual Machine 4', 'name': 'Virtual Machine 4',
'site': sites[1].pk,
'cluster': clusters[1].pk, 'cluster': clusters[1].pk,
'device': device2.pk, 'device': device2.pk,
}, },
{ {
'name': 'Virtual Machine 5', 'name': 'Virtual Machine 5',
'site': sites[1].pk,
'cluster': clusters[1].pk, 'cluster': clusters[1].pk,
}, },
{ {
'name': 'Virtual Machine 6', 'name': 'Virtual Machine 6',
'cluster': clusters[1].pk, 'site': sites[1].pk,
},
{
'name': 'Virtual Machine 7',
'cluster': clusters[2].pk,
}, },
] ]

View File

@ -274,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], 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 1', site=sites[0], 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], 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 2', site=sites[1], 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], device=devices[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', site=sites[2], 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)

View File

@ -1,21 +1,19 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from dcim.models import Site
from virtualization.models import * from virtualization.models import *
from tenancy.models import Tenant from tenancy.models import Tenant
class VirtualMachineTestCase(TestCase): class VirtualMachineTestCase(TestCase):
def setUp(self):
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='Test Cluster Type 1')
self.cluster = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
def test_vm_duplicate_name_per_cluster(self): def test_vm_duplicate_name_per_cluster(self):
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster = Cluster.objects.create(name='Cluster 1', type=cluster_type)
vm1 = VirtualMachine( vm1 = VirtualMachine(
cluster=self.cluster, cluster=cluster,
name='Test VM 1' name='Test VM 1'
) )
vm1.save() vm1.save()
@ -43,3 +41,33 @@ class VirtualMachineTestCase(TestCase):
# Two VMs assigned to the same Cluster and different Tenants should pass validation # Two VMs assigned to the same Cluster and different Tenants should pass validation
vm2.full_clean() vm2.full_clean()
vm2.save() vm2.save()
def test_vm_mismatched_site_cluster(self):
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
clusters = (
Cluster(name='Cluster 1', type=cluster_type, site=sites[0]),
Cluster(name='Cluster 2', type=cluster_type, site=sites[1]),
Cluster(name='Cluster 3', type=cluster_type, site=None),
)
Cluster.objects.bulk_create(clusters)
# VM with site only should pass
VirtualMachine(name='vm1', site=sites[0]).full_clean()
# VM with non-site cluster only should pass
VirtualMachine(name='vm1', cluster=clusters[2]).full_clean()
# VM with mismatched site & cluster should fail
with self.assertRaises(ValidationError):
VirtualMachine(name='vm1', site=sites[0], cluster=clusters[1]).full_clean()
# VM with cluster site but no direct site should fail
with self.assertRaises(ValidationError):
VirtualMachine(name='vm1', site=None, cluster=clusters[0]).full_clean()

View File

@ -168,23 +168,29 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
Platform.objects.bulk_create(platforms) Platform.objects.bulk_create(platforms)
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
clusters = ( clusters = (
Cluster(name='Cluster 1', type=clustertype), Cluster(name='Cluster 1', type=clustertype, site=sites[0]),
Cluster(name='Cluster 2', type=clustertype), Cluster(name='Cluster 2', type=clustertype, site=sites[1]),
) )
Cluster.objects.bulk_create(clusters) Cluster.objects.bulk_create(clusters)
devices = ( devices = (
create_test_device('device1', cluster=clusters[0]), create_test_device('device1', site=sites[0], cluster=clusters[0]),
create_test_device('device2', cluster=clusters[1]), create_test_device('device2', site=sites[1], cluster=clusters[1]),
) )
VirtualMachine.objects.bulk_create([ VirtualMachine.objects.bulk_create([
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]), VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=devices[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 2', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]), VirtualMachine(name='Virtual Machine 3', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
@ -192,6 +198,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = { cls.form_data = {
'cluster': clusters[1].pk, 'cluster': clusters[1].pk,
'device': devices[1].pk, 'device': devices[1].pk,
'site': sites[1].pk,
'tenant': None, 'tenant': None,
'platform': platforms[1].pk, 'platform': platforms[1].pk,
'name': 'Virtual Machine X', 'name': 'Virtual Machine X',
@ -208,13 +215,14 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"name,status,cluster,device", "name,status,site,cluster,device",
"Virtual Machine 4,active,Cluster 1,device1", "Virtual Machine 4,active,Site 1,Cluster 1,device1",
"Virtual Machine 5,active,Cluster 1,device1", "Virtual Machine 5,active,Site 1,Cluster 1,device1",
"Virtual Machine 6,active,Cluster 1,", "Virtual Machine 6,active,Site 1,Cluster 1,",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'site': sites[1].pk,
'cluster': clusters[1].pk, 'cluster': clusters[1].pk,
'device': devices[1].pk, 'device': devices[1].pk,
'tenant': None, 'tenant': None,
@ -252,8 +260,8 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site) cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site)
virtualmachines = ( virtualmachines = (
VirtualMachine(name='Virtual Machine 1', cluster=cluster, role=devicerole), VirtualMachine(name='Virtual Machine 1', site=site, cluster=cluster, role=devicerole),
VirtualMachine(name='Virtual Machine 2', cluster=cluster, role=devicerole), VirtualMachine(name='Virtual Machine 2', site=site, cluster=cluster, role=devicerole),
) )
VirtualMachine.objects.bulk_create(virtualmachines) VirtualMachine.objects.bulk_create(virtualmachines)