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:
@ -1,6 +1,6 @@
|
||||
# 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:
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
### Enhancements
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
@ -30,3 +31,5 @@
|
||||
* Added required `status` field (default value: `active`)
|
||||
* virtualization.VirtualMachine
|
||||
* 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.
|
||||
|
@ -81,13 +81,19 @@
|
||||
<h5 class="card-header">Cluster</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Site</th>
|
||||
<td>
|
||||
{{ object.site|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Cluster</th>
|
||||
<td>
|
||||
{% if object.cluster.group %}
|
||||
{{ object.cluster.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.cluster|linkify }}
|
||||
{{ object.cluster|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -34,10 +34,11 @@ def post_data(data):
|
||||
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).
|
||||
"""
|
||||
if site is None:
|
||||
site, _ = Site.objects.get_or_create(name='Site 1', slug='site-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)
|
||||
|
@ -68,8 +68,8 @@ class ClusterSerializer(NetBoxModelSerializer):
|
||||
class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||
site = NestedSiteSerializer(read_only=True)
|
||||
cluster = NestedClusterSerializer()
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
|
@ -54,7 +54,7 @@ class ClusterViewSet(NetBoxModelViewSet):
|
||||
|
||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||
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
|
||||
|
||||
|
@ -162,37 +162,36 @@ class VirtualMachineFilterSet(
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='cluster__site__region',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='cluster__site__region',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='cluster__site__group',
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='cluster__site__group',
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
|
@ -106,9 +106,16 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@ -153,11 +160,11 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = VirtualMachine
|
||||
fieldsets = (
|
||||
(None, ('cluster', 'device', 'status', 'role', 'tenant', 'platform')),
|
||||
(None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform')),
|
||||
('Resources', ('vcpus', 'memory', 'disk'))
|
||||
)
|
||||
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
|
||||
if 'pk' in self.initial:
|
||||
site = None
|
||||
interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related(
|
||||
'virtual_machine__cluster__site'
|
||||
interfaces = VMInterface.objects.filter(
|
||||
pk__in=self.initial['pk']
|
||||
).prefetch_related(
|
||||
'virtual_machine__site'
|
||||
)
|
||||
|
||||
# Check interface sites. First interface should set site, further interfaces will either continue the
|
||||
|
@ -71,9 +71,16 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
||||
choices=VirtualMachineStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Assigned site'
|
||||
)
|
||||
cluster = CSVModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Assigned cluster'
|
||||
)
|
||||
device = CSVModelChoiceField(
|
||||
@ -106,7 +113,8 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'name', 'status', 'role', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||
'comments',
|
||||
)
|
||||
|
||||
|
||||
|
@ -165,6 +165,9 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
|
||||
|
||||
|
||||
class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all()
|
||||
)
|
||||
cluster_group = DynamicModelChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
@ -176,7 +179,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
||||
cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
query_params={
|
||||
'group_id': '$cluster_group'
|
||||
'site_id': '$site',
|
||||
'group_id': '$cluster_group',
|
||||
}
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
@ -204,7 +208,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
||||
('Cluster', ('cluster_group', 'cluster', 'device')),
|
||||
('Cluster', ('site', 'cluster_group', 'cluster', 'device')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
||||
('Resources', ('vcpus', 'memory', 'disk')),
|
||||
@ -214,8 +218,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'name', 'status', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform',
|
||||
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
|
||||
'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags',
|
||||
'local_context_data',
|
||||
]
|
||||
help_texts = {
|
||||
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
),
|
||||
]
|
@ -195,10 +195,19 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
"""
|
||||
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(
|
||||
to='virtualization.Cluster',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_machines'
|
||||
related_name='virtual_machines',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
@ -291,7 +300,7 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
||||
'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -323,6 +332,22 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
def clean(self):
|
||||
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
|
||||
if self.device and self.device not in self.cluster.devices.all():
|
||||
raise ValidationError({
|
||||
@ -357,10 +382,6 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def site(self):
|
||||
return self.cluster.site
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
|
@ -30,6 +30,9 @@ class VirtualMachineTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
cluster = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -59,11 +62,11 @@ class VirtualMachineTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory',
|
||||
'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -2,6 +2,7 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.models import Site
|
||||
from ipam.models import VLAN, VRF
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||
from virtualization.choices import *
|
||||
@ -146,39 +147,49 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-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 = (
|
||||
Cluster(name='Cluster 1', type=clustertype, group=clustergroup),
|
||||
Cluster(name='Cluster 2', type=clustertype, group=clustergroup),
|
||||
Cluster(name='Cluster 1', type=clustertype, site=sites[0], group=clustergroup),
|
||||
Cluster(name='Cluster 2', type=clustertype, site=sites[1], group=clustergroup),
|
||||
Cluster(name='Cluster 3', type=clustertype),
|
||||
)
|
||||
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()
|
||||
device1 = create_test_device('device1', site=sites[0], cluster=clusters[0])
|
||||
device2 = create_test_device('device2', site=sites[1], cluster=clusters[1])
|
||||
|
||||
virtual_machines = (
|
||||
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 3', cluster=clusters[0], local_context_data={'C': 3}),
|
||||
VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=device1, local_context_data={'A': 1}),
|
||||
VirtualMachine(name='Virtual Machine 2', site=sites[0], cluster=clusters[0], local_context_data={'B': 2}),
|
||||
VirtualMachine(name='Virtual Machine 3', site=sites[0], cluster=clusters[0], local_context_data={'C': 3}),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Virtual Machine 4',
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
'device': device2.pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 5',
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 6',
|
||||
'cluster': clusters[1].pk,
|
||||
'site': sites[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 7',
|
||||
'cluster': clusters[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -274,9 +274,9 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
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 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], 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 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', 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', 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)
|
||||
|
||||
|
@ -1,21 +1,19 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Site
|
||||
from virtualization.models import *
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
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):
|
||||
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(
|
||||
cluster=self.cluster,
|
||||
cluster=cluster,
|
||||
name='Test VM 1'
|
||||
)
|
||||
vm1.save()
|
||||
@ -43,3 +41,33 @@ class VirtualMachineTestCase(TestCase):
|
||||
# Two VMs assigned to the same Cluster and different Tenants should pass validation
|
||||
vm2.full_clean()
|
||||
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()
|
||||
|
@ -168,23 +168,29 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
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')
|
||||
|
||||
clusters = (
|
||||
Cluster(name='Cluster 1', type=clustertype),
|
||||
Cluster(name='Cluster 2', type=clustertype),
|
||||
Cluster(name='Cluster 1', type=clustertype, site=sites[0]),
|
||||
Cluster(name='Cluster 2', type=clustertype, site=sites[1]),
|
||||
)
|
||||
Cluster.objects.bulk_create(clusters)
|
||||
|
||||
devices = (
|
||||
create_test_device('device1', cluster=clusters[0]),
|
||||
create_test_device('device2', cluster=clusters[1]),
|
||||
create_test_device('device1', site=sites[0], cluster=clusters[0]),
|
||||
create_test_device('device2', site=sites[1], cluster=clusters[1]),
|
||||
)
|
||||
|
||||
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 2', 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 1', site=sites[0], 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', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||
])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
@ -192,6 +198,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'cluster': clusters[1].pk,
|
||||
'device': devices[1].pk,
|
||||
'site': sites[1].pk,
|
||||
'tenant': None,
|
||||
'platform': platforms[1].pk,
|
||||
'name': 'Virtual Machine X',
|
||||
@ -208,13 +215,14 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,status,cluster,device",
|
||||
"Virtual Machine 4,active,Cluster 1,device1",
|
||||
"Virtual Machine 5,active,Cluster 1,device1",
|
||||
"Virtual Machine 6,active,Cluster 1,",
|
||||
"name,status,site,cluster,device",
|
||||
"Virtual Machine 4,active,Site 1,Cluster 1,device1",
|
||||
"Virtual Machine 5,active,Site 1,Cluster 1,device1",
|
||||
"Virtual Machine 6,active,Site 1,Cluster 1,",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
'device': devices[1].pk,
|
||||
'tenant': None,
|
||||
@ -252,8 +260,8 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site)
|
||||
virtualmachines = (
|
||||
VirtualMachine(name='Virtual Machine 1', cluster=cluster, role=devicerole),
|
||||
VirtualMachine(name='Virtual Machine 2', cluster=cluster, role=devicerole),
|
||||
VirtualMachine(name='Virtual Machine 1', site=site, cluster=cluster, role=devicerole),
|
||||
VirtualMachine(name='Virtual Machine 2', site=site, cluster=cluster, role=devicerole),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtualmachines)
|
||||
|
||||
|
Reference in New Issue
Block a user