mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-11 03:37:06 +02:00
Merge branch 'feature' into 21357-register-model-actions
This commit is contained in:
@@ -1305,6 +1305,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'virtualdevicecontext',
|
||||
'virtualdisk',
|
||||
'virtualmachine',
|
||||
'virtualmachinetype',
|
||||
'vlan',
|
||||
'vlangroup',
|
||||
'vlantranslationpolicy',
|
||||
|
||||
@@ -268,6 +268,7 @@ VIRTUALIZATION_MENU = Menu(
|
||||
get_model_item('virtualization', 'virtualmachine', _('Virtual Machines')),
|
||||
get_model_item('virtualization', 'vminterface', _('Interfaces')),
|
||||
get_model_item('virtualization', 'virtualdisk', _('Virtual Disks')),
|
||||
get_model_item('virtualization', 'virtualmachinetype', _('Virtual Machine Types')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
|
||||
10
netbox/templates/virtualization/virtualmachinetype.html
Normal file
10
netbox/templates/virtualization/virtualmachinetype.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.virtualization.add_virtualmachine %}
|
||||
<a href="{% url 'virtualization:virtualmachine_add' %}?virtual_machine_type={{ object.pk }}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Virtual Machine" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
@@ -16,10 +16,10 @@ from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from users.api.serializers_.mixins import OwnerMixin
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
|
||||
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||
|
||||
from ...choices import *
|
||||
from ...models import VirtualDisk, VirtualMachine, VirtualMachineType, VMInterface
|
||||
from .clusters import ClusterSerializer
|
||||
from .nested import NestedVMInterfaceSerializer
|
||||
|
||||
@@ -27,11 +27,29 @@ __all__ = (
|
||||
'VMInterfaceSerializer',
|
||||
'VirtualDiskSerializer',
|
||||
'VirtualMachineSerializer',
|
||||
'VirtualMachineTypeSerializer',
|
||||
'VirtualMachineWithConfigContextSerializer',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineTypeSerializer(PrimaryModelSerializer):
|
||||
default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
# Counter fields
|
||||
virtual_machine_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachineType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'default_platform', 'default_vcpus',
|
||||
'default_memory', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'virtual_machine_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class VirtualMachineSerializer(PrimaryModelSerializer):
|
||||
virtual_machine_type = VirtualMachineTypeSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||
start_on_boot = ChoiceField(choices=VirtualMachineStartOnBootChoices, required=False)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
@@ -52,10 +70,10 @@ class VirtualMachineSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device',
|
||||
'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
|
||||
'disk', 'description', 'owner', 'comments', 'config_template', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot',
|
||||
'site', 'cluster', 'device', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
|
||||
'disk', 'description', 'serial', 'tenant', 'owner', 'comments', 'tags', 'local_context_data',
|
||||
'config_template', 'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@@ -65,10 +83,11 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
|
||||
class Meta(VirtualMachineSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device',
|
||||
'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
|
||||
'disk', 'description', 'owner', 'comments', 'config_template', 'local_context_data', 'tags',
|
||||
'custom_fields', 'config_context', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot',
|
||||
'site', 'cluster', 'device', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
|
||||
'disk', 'description', 'serial', 'tenant', 'owner', 'comments', 'tags', 'local_context_data',
|
||||
'config_template', 'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
|
||||
'config_context',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
|
||||
@@ -10,6 +10,9 @@ router.register('cluster-types', views.ClusterTypeViewSet)
|
||||
router.register('cluster-groups', views.ClusterGroupViewSet)
|
||||
router.register('clusters', views.ClusterViewSet)
|
||||
|
||||
# Virtual machine types
|
||||
router.register('virtual-machine-types', views.VirtualMachineTypeViewSet)
|
||||
|
||||
# VirtualMachines
|
||||
router.register('virtual-machines', views.VirtualMachineViewSet)
|
||||
router.register('interfaces', views.VMInterfaceViewSet)
|
||||
|
||||
@@ -14,6 +14,7 @@ class VirtualizationRootView(APIRootView):
|
||||
"""
|
||||
Virtualization API root view
|
||||
"""
|
||||
|
||||
def get_view_name(self):
|
||||
return 'Virtualization'
|
||||
|
||||
@@ -22,6 +23,7 @@ class VirtualizationRootView(APIRootView):
|
||||
# Clusters
|
||||
#
|
||||
|
||||
|
||||
class ClusterTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = ClusterType.objects.all()
|
||||
serializer_class = serializers.ClusterTypeSerializer
|
||||
@@ -44,10 +46,22 @@ class ClusterViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.ClusterFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual machine types
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachineTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
serializer_class = serializers.VirtualMachineTypeSerializer
|
||||
filterset_class = filtersets.VirtualMachineTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
|
||||
queryset = VirtualMachine.objects.all()
|
||||
filterset_class = filtersets.VirtualMachineFilterSet
|
||||
|
||||
@@ -12,13 +12,13 @@ class VirtualizationConfig(AppConfig):
|
||||
from utilities.permissions import ModelAction, register_model_actions
|
||||
|
||||
from . import search, signals # noqa: F401
|
||||
from .models import VirtualMachine
|
||||
from .models import VirtualMachine, VirtualMachineType
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register counters
|
||||
connect_counters(VirtualMachine)
|
||||
connect_counters(VirtualMachine, VirtualMachineType)
|
||||
|
||||
# Register custom permission actions
|
||||
register_model_actions(VirtualMachine, [
|
||||
|
||||
@@ -26,6 +26,7 @@ __all__ = (
|
||||
'VMInterfaceFilterSet',
|
||||
'VirtualDiskFilterSet',
|
||||
'VirtualMachineFilterSet',
|
||||
'VirtualMachineTypeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@@ -91,6 +92,45 @@ class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet,
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualMachineTypeFilterSet(PrimaryModelFilterSet):
|
||||
default_platform_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
field_name='default_platform',
|
||||
lookup_expr='in',
|
||||
label=_('Default platform (ID)'),
|
||||
)
|
||||
default_platform = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
field_name='default_platform',
|
||||
to_field_name='slug',
|
||||
lookup_expr='in',
|
||||
label=_('Default platform (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachineType
|
||||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'default_vcpus',
|
||||
'default_memory',
|
||||
'description',
|
||||
'virtual_machine_count',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualMachineFilterSet(
|
||||
PrimaryModelFilterSet,
|
||||
@@ -99,6 +139,18 @@ class VirtualMachineFilterSet(
|
||||
LocalConfigContextFilterSet,
|
||||
PrimaryIPFilterSet,
|
||||
):
|
||||
virtual_machine_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual machine type (ID)'),
|
||||
)
|
||||
virtual_machine_type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine_type__slug',
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Virtual machine type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualMachineStatusChoices,
|
||||
distinct=False,
|
||||
|
||||
@@ -14,8 +14,9 @@ from utilities.forms import BulkRenameForm, add_blank_choice
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
|
||||
from ..choices import *
|
||||
from ..models import *
|
||||
|
||||
__all__ = (
|
||||
'ClusterBulkEditForm',
|
||||
@@ -26,6 +27,7 @@ __all__ = (
|
||||
'VirtualDiskBulkEditForm',
|
||||
'VirtualDiskBulkRenameForm',
|
||||
'VirtualMachineBulkEditForm',
|
||||
'VirtualMachineTypeBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +80,37 @@ class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineTypeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
default_platform = DynamicModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False
|
||||
)
|
||||
default_vcpus = forms.IntegerField(
|
||||
label=_('Default vCPUs'),
|
||||
required=False,
|
||||
)
|
||||
default_memory = forms.IntegerField(
|
||||
label=_('Default Memory (MB)'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
model = VirtualMachineType
|
||||
fieldsets = (
|
||||
FieldSet('description', name=_('Virtual Machine Type')),
|
||||
FieldSet('default_platform', 'default_vcpus', 'default_memory', name=_('Defaults')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'default_platform', 'default_vcpus', 'default_memory', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
|
||||
virtual_machine_type = DynamicModelChoiceField(
|
||||
label=_('Virtual machine type'),
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=add_blank_choice(VirtualMachineStatusChoices),
|
||||
@@ -152,13 +184,14 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
model = VirtualMachine
|
||||
fieldsets = (
|
||||
FieldSet('status', 'start_on_boot', 'role', 'tenant', 'platform', 'description'),
|
||||
FieldSet('virtual_machine_type', 'status', 'start_on_boot', 'role', 'tenant', 'platform', 'description'),
|
||||
FieldSet('site', 'cluster', 'device', name=_('Placement')),
|
||||
FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
|
||||
FieldSet('config_template', name=_('Configuration')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
|
||||
'virtual_machine_type', 'role', 'site', 'cluster', 'device', 'platform', 'vcpus', 'memory', 'disk', 'tenant',
|
||||
'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ from ipam.models import VLAN, VRF, VLANGroup
|
||||
from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, OwnerCSVMixin, PrimaryModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
|
||||
from ..choices import *
|
||||
from ..models import *
|
||||
|
||||
__all__ = (
|
||||
'ClusterGroupImportForm',
|
||||
@@ -20,6 +21,7 @@ __all__ = (
|
||||
'VMInterfaceImportForm',
|
||||
'VirtualDiskImportForm',
|
||||
'VirtualMachineImportForm',
|
||||
'VirtualMachineTypeImportForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -82,7 +84,31 @@ class ClusterImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
}
|
||||
|
||||
|
||||
class VirtualMachineTypeImportForm(PrimaryModelImportForm):
|
||||
default_platform = CSVModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned default platform'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachineType
|
||||
fields = (
|
||||
'name', 'slug', 'default_platform', 'default_vcpus', 'default_memory', 'description',
|
||||
'owner', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineImportForm(PrimaryModelImportForm):
|
||||
virtual_machine_type = CSVModelChoiceField(
|
||||
label=_('Virtual machine type'),
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text=_('Optional virtual machine type'),
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=VirtualMachineStatusChoices,
|
||||
@@ -149,8 +175,9 @@ class VirtualMachineImportForm(PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'name', 'status', 'start_on_boot', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus',
|
||||
'memory', 'disk', 'description', 'serial', 'config_template', 'comments', 'owner', 'tags',
|
||||
'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot', 'site', 'cluster', 'device',
|
||||
'platform', 'vcpus', 'memory', 'disk', 'description', 'serial',
|
||||
'tenant', 'owner', 'comments', 'tags', 'config_template',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
from vpn.models import L2VPN
|
||||
|
||||
from ..choices import *
|
||||
from ..models import *
|
||||
|
||||
__all__ = (
|
||||
'ClusterFilterForm',
|
||||
'ClusterGroupFilterForm',
|
||||
@@ -23,6 +24,7 @@ __all__ = (
|
||||
'VMInterfaceFilterForm',
|
||||
'VirtualDiskFilterForm',
|
||||
'VirtualMachineFilterForm',
|
||||
'VirtualMachineTypeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -100,6 +102,43 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelF
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualMachineTypeFilterForm(PrimaryModelFilterSetForm):
|
||||
model = VirtualMachineType
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'default_platform_id', 'default_vcpus', 'default_memory', 'virtual_machine_count',
|
||||
name=_('Attributes'),
|
||||
),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
|
||||
selector_fields = ('filter_id', 'q')
|
||||
|
||||
default_platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
label=_('Default platform'),
|
||||
)
|
||||
default_vcpus = forms.DecimalField(
|
||||
label=_('Default vCPUs'),
|
||||
required=False,
|
||||
)
|
||||
default_memory = forms.IntegerField(
|
||||
label=_('Default memory (MB)'),
|
||||
required=False,
|
||||
min_value=0,
|
||||
)
|
||||
virtual_machine_count = forms.IntegerField(
|
||||
label=_('Virtual machine count'),
|
||||
required=False,
|
||||
min_value=0,
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualMachineFilterForm(
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
@@ -112,13 +151,20 @@ class VirtualMachineFilterForm(
|
||||
FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet(
|
||||
'status', 'start_on_boot', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id',
|
||||
'local_context_data', 'serial', name=_('Attributes')
|
||||
'virtual_machine_type_id', 'status', 'start_on_boot', 'role_id', 'platform_id', 'mac_address',
|
||||
'has_primary_ip', 'config_template_id', 'local_context_data', 'serial',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
virtual_machine_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Virtual machine type'),
|
||||
)
|
||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -13,11 +13,12 @@ from ipam.models import VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy
|
||||
from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
|
||||
from netbox.forms.mixins import OwnerMixin
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
from virtualization.models import *
|
||||
|
||||
from ..models import *
|
||||
|
||||
__all__ = (
|
||||
'ClusterAddDevicesForm',
|
||||
@@ -28,6 +29,7 @@ __all__ = (
|
||||
'VMInterfaceForm',
|
||||
'VirtualDiskForm',
|
||||
'VirtualMachineForm',
|
||||
'VirtualMachineTypeForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -167,7 +169,35 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineTypeForm(PrimaryModelForm):
|
||||
slug = SlugField()
|
||||
default_platform = DynamicModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
selector=True,
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('Virtual Machine Type')),
|
||||
FieldSet('default_platform', 'default_vcpus', 'default_memory', name=_('Defaults')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachineType
|
||||
fields = (
|
||||
'name', 'slug', 'default_platform', 'default_vcpus', 'default_memory', 'description',
|
||||
'owner', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineForm(TenancyForm, PrimaryModelForm):
|
||||
virtual_machine_type = forms.ModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualMachineType.objects.all(),
|
||||
required=False,
|
||||
widget=HTMXSelect(),
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
@@ -226,7 +256,10 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'role', 'status', 'start_on_boot', 'description', 'serial', 'tags', name=_('Virtual Machine')),
|
||||
FieldSet(
|
||||
'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot', 'description', 'serial', 'tags',
|
||||
name=_('Virtual Machine')
|
||||
),
|
||||
FieldSet('site', 'cluster', 'device', name=_('Placement')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')),
|
||||
@@ -237,14 +270,17 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner',
|
||||
'comments', 'tags', 'local_context_data', 'config_template',
|
||||
'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot', 'site', 'cluster', 'device',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial',
|
||||
'tenant_group', 'tenant', 'owner', 'comments', 'tags', 'local_context_data', 'config_template',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Populate virtual machine type defaults, if any
|
||||
self._populate_virtual_machine_type_defaults()
|
||||
|
||||
if self.instance.pk:
|
||||
|
||||
# Disable the disk field if one or more VirtualDisks have been created
|
||||
@@ -285,6 +321,36 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
|
||||
self.fields.pop('primary_ip4')
|
||||
self.fields.pop('primary_ip6')
|
||||
|
||||
def _populate_virtual_machine_type_defaults(self):
|
||||
"""
|
||||
Populate platform/vCPUs/memory from the selected VirtualMachineType.
|
||||
|
||||
For new VMs, always apply defaults. For existing VMs, only apply
|
||||
defaults when the type changes and the field is currently empty.
|
||||
"""
|
||||
if not (virtual_machine_type_id := get_field_value(self, 'virtual_machine_type')):
|
||||
return
|
||||
|
||||
is_new = not self.instance.pk
|
||||
|
||||
# Skip if editing and the type hasn't changed
|
||||
if not is_new and int(virtual_machine_type_id) == self.instance.virtual_machine_type_id:
|
||||
return
|
||||
|
||||
if not (virtual_machine_type := VirtualMachineType.objects.filter(pk=virtual_machine_type_id).first()):
|
||||
return
|
||||
|
||||
defaults = {
|
||||
'platform': ('default_platform_id', 'platform_id'),
|
||||
'vcpus': ('default_vcpus', 'vcpus'),
|
||||
'memory': ('default_memory', 'memory'),
|
||||
}
|
||||
|
||||
for field_name, (type_attr, instance_attr) in defaults.items():
|
||||
default_value = getattr(virtual_machine_type, type_attr)
|
||||
if default_value is not None and (is_new or getattr(self.instance, instance_attr) is None):
|
||||
self.initial[field_name] = default_value
|
||||
|
||||
|
||||
#
|
||||
# Virtual machine components
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup, StrFilterLookup
|
||||
|
||||
from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin
|
||||
from extras.graphql.filter_mixins import ConfigContextFilterMixin
|
||||
@@ -34,6 +34,7 @@ __all__ = (
|
||||
'VMInterfaceFilter',
|
||||
'VirtualDiskFilter',
|
||||
'VirtualMachineFilter',
|
||||
'VirtualMachineTypeFilter',
|
||||
)
|
||||
|
||||
|
||||
@@ -68,6 +69,24 @@ class ClusterTypeFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualMachineType, lookups=True)
|
||||
class VirtualMachineTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilter):
|
||||
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
default_platform_id: ID | None = strawberry_django.filter_field()
|
||||
default_vcpus: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
default_memory: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
instances: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
virtual_machine_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualMachine, lookups=True)
|
||||
class VirtualMachineFilter(
|
||||
ContactFilterMixin,
|
||||
@@ -78,6 +97,9 @@ class VirtualMachineFilter(
|
||||
PrimaryModelFilter,
|
||||
):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
virtual_machine_type: (
|
||||
Annotated['VirtualMachineTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
|
||||
@@ -4,7 +4,7 @@ import strawberry_django
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type(name="Query")
|
||||
@strawberry.type(name='Query')
|
||||
class VirtualizationQuery:
|
||||
cluster: ClusterType = strawberry_django.field()
|
||||
cluster_list: list[ClusterType] = strawberry_django.field()
|
||||
@@ -15,6 +15,9 @@ class VirtualizationQuery:
|
||||
cluster_type: ClusterTypeType = strawberry_django.field()
|
||||
cluster_type_list: list[ClusterTypeType] = strawberry_django.field()
|
||||
|
||||
virtual_machine_type: VirtualMachineTypeType = strawberry_django.field()
|
||||
virtual_machine_type_list: list[VirtualMachineTypeType] = strawberry_django.field()
|
||||
|
||||
virtual_machine: VirtualMachineType = strawberry_django.field()
|
||||
virtual_machine_list: list[VirtualMachineType] = strawberry_django.field()
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ __all__ = (
|
||||
'VMInterfaceType',
|
||||
'VirtualDiskType',
|
||||
'VirtualMachineType',
|
||||
'VirtualMachineTypeType',
|
||||
)
|
||||
|
||||
|
||||
@@ -91,6 +92,19 @@ class ClusterTypeType(OrganizationalObjectType):
|
||||
clusters: list[ClusterType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualMachineType,
|
||||
fields='__all__',
|
||||
filters=VirtualMachineTypeFilter,
|
||||
pagination=True
|
||||
)
|
||||
class VirtualMachineTypeType(PrimaryObjectType):
|
||||
virtual_machine_count: BigInt
|
||||
default_platform: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
instances: list[Annotated['VirtualMachineType', strawberry.lazy('virtualization.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualMachine,
|
||||
fields='__all__',
|
||||
@@ -101,6 +115,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, PrimaryObjectType):
|
||||
interface_count: BigInt
|
||||
virtual_disk_count: BigInt
|
||||
interface_count: BigInt
|
||||
virtual_machine_type: Annotated['VirtualMachineTypeType', strawberry.lazy('virtualization.graphql.types')] | None
|
||||
config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
cluster: Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
|
||||
106
netbox/virtualization/migrations/0054_virtualmachinetype.py
Normal file
106
netbox/virtualization/migrations/0054_virtualmachinetype.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.db.models.functions.text
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import netbox.models.deletion
|
||||
import utilities.fields
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('virtualization', '0053_virtualmachine_standalone_device_assignment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualMachineType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100)),
|
||||
(
|
||||
'default_vcpus',
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
max_digits=6,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
|
||||
),
|
||||
),
|
||||
('default_memory', models.PositiveIntegerField(blank=True, null=True)),
|
||||
(
|
||||
'virtual_machine_count',
|
||||
utilities.fields.CounterCacheField(
|
||||
default=0,
|
||||
editable=False,
|
||||
to_field='virtual_machine_type',
|
||||
to_model='virtualization.VirtualMachine',
|
||||
),
|
||||
),
|
||||
(
|
||||
'default_platform',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='dcim.platform',
|
||||
),
|
||||
),
|
||||
(
|
||||
'owner',
|
||||
models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual machine type',
|
||||
'verbose_name_plural': 'virtual machine types',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='virtual_machine_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='instances',
|
||||
to='virtualization.virtualmachinetype',
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualmachinetype',
|
||||
constraint=models.UniqueConstraint(
|
||||
django.db.models.functions.text.Lower('name'),
|
||||
name='virtualization_virtualmachinetype_unique_name',
|
||||
violation_error_message='Virtual machine type name must be unique.',
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualmachinetype',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('slug',),
|
||||
name='virtualization_virtualmachinetype_unique_slug',
|
||||
violation_error_message='Virtual machine type slug must be unique.',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -20,16 +20,88 @@ from utilities.fields import CounterCacheField, NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
from virtualization.choices import *
|
||||
|
||||
from ..choices import *
|
||||
|
||||
__all__ = (
|
||||
'VMInterface',
|
||||
'VirtualDisk',
|
||||
'VirtualMachine',
|
||||
'VirtualMachineType',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, ConfigContextModel, PrimaryModel):
|
||||
class VirtualMachineType(ImageAttachmentsMixin, PrimaryModel):
|
||||
"""
|
||||
A type defining default attributes (platform, vCPUs, memory, etc.) for virtual machines.
|
||||
"""
|
||||
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100,
|
||||
)
|
||||
slug = models.SlugField(
|
||||
verbose_name=_('slug'),
|
||||
max_length=100,
|
||||
)
|
||||
default_platform = models.ForeignKey(
|
||||
to='dcim.Platform',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('default platform'),
|
||||
)
|
||||
default_vcpus = models.DecimalField(
|
||||
verbose_name=_('default vCPUs'),
|
||||
max_digits=6,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(MinValueValidator(decimal.Decimal('0.01')),),
|
||||
)
|
||||
default_memory = models.PositiveIntegerField(
|
||||
verbose_name=_('default memory (MB)'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
# Counter fields
|
||||
virtual_machine_count = CounterCacheField(
|
||||
to_model='virtualization.VirtualMachine',
|
||||
to_field='virtual_machine_type',
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'default_platform',
|
||||
'default_vcpus',
|
||||
'default_memory',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
Lower('name'),
|
||||
name='%(app_label)s_%(class)s_unique_name',
|
||||
violation_error_message=_('Virtual machine type name must be unique.'),
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('slug',),
|
||||
name='%(app_label)s_%(class)s_unique_slug',
|
||||
violation_error_message=_('Virtual machine type slug must be unique.'),
|
||||
),
|
||||
)
|
||||
verbose_name = _('virtual machine type')
|
||||
verbose_name_plural = _('virtual machine types')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class VirtualMachine(
|
||||
ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, ConfigContextModel, TrackingModelMixin, PrimaryModel
|
||||
):
|
||||
"""
|
||||
A virtual machine which runs on a Cluster or a standalone Device.
|
||||
|
||||
@@ -42,6 +114,15 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
||||
When a Cluster or Device is set, the Site is automatically inherited if not explicitly provided.
|
||||
If a Device belongs to a Cluster, the Cluster must also be specified on the VM.
|
||||
"""
|
||||
|
||||
virtual_machine_type = models.ForeignKey(
|
||||
to='virtualization.VirtualMachineType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='instances',
|
||||
verbose_name=_('type'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -162,7 +243,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
clone_fields = (
|
||||
'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
||||
'virtual_machine_type', 'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory',
|
||||
'disk',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -282,8 +364,29 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
||||
elif self.device and self.device.site:
|
||||
self.site = self.device.site
|
||||
|
||||
if self._state.adding:
|
||||
self.apply_type_defaults()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def apply_type_defaults(self):
|
||||
"""
|
||||
Populate any empty fields with defaults from the assigned VirtualMachineType.
|
||||
"""
|
||||
if not self.virtual_machine_type_id:
|
||||
return
|
||||
|
||||
defaults = {
|
||||
'platform_id': 'default_platform_id',
|
||||
'vcpus': 'default_vcpus',
|
||||
'memory': 'default_memory',
|
||||
}
|
||||
for field, default_field in defaults.items():
|
||||
if getattr(self, field) is None:
|
||||
default_value = getattr(self.virtual_machine_type, default_field)
|
||||
if default_value is not None:
|
||||
setattr(self, field, default_value)
|
||||
|
||||
def get_status_color(self):
|
||||
return VirtualMachineStatusChoices.colors.get(self.status)
|
||||
|
||||
|
||||
@@ -38,6 +38,18 @@ class ClusterTypeIndex(SearchIndex):
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualMachineTypeIndex(SearchIndex):
|
||||
model = models.VirtualMachineType
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('default_platform', 'default_vcpus', 'default_memory', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualMachineIndex(SearchIndex):
|
||||
model = models.VirtualMachine
|
||||
|
||||
@@ -5,28 +5,64 @@ from dcim.tables.devices import BaseInterfaceTable
|
||||
from netbox.tables import NetBoxTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
from utilities.templatetags.helpers import humanize_disk_megabytes
|
||||
from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
|
||||
|
||||
from ..models import VirtualDisk, VirtualMachine, VirtualMachineType, VMInterface
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
'VMInterfaceTable',
|
||||
'VirtualDiskTable',
|
||||
'VirtualMachineTable',
|
||||
'VirtualMachineTypeTable',
|
||||
'VirtualMachineVMInterfaceTable',
|
||||
'VirtualMachineVirtualDiskTable',
|
||||
)
|
||||
|
||||
#
|
||||
# Virtual machine types
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachineTypeTable(PrimaryModelTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True,
|
||||
)
|
||||
default_platform = tables.Column(
|
||||
verbose_name=_('Default platform'),
|
||||
linkify=True,
|
||||
)
|
||||
virtual_machine_count = tables.Column(
|
||||
verbose_name=_('VM count')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='virtualization:virtualmachinetype_list'
|
||||
)
|
||||
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = VirtualMachineType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'default_platform', 'default_vcpus', 'default_memory', 'virtual_machine_count',
|
||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'default_platform', 'default_vcpus', 'default_memory', 'virtual_machine_count', 'description',
|
||||
)
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
virtual_machine_type = tables.Column(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True,
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
)
|
||||
@@ -85,12 +121,13 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant',
|
||||
'tenant_group', 'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description',
|
||||
'comments', 'config_template', 'serial', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot', 'site', 'cluster', 'device',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'primary_ip', 'vcpus', 'memory', 'disk', 'description', 'serial',
|
||||
'tenant_group', 'tenant', 'contacts', 'comments', 'tags', 'config_template', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
||||
'pk', 'name', 'virtual_machine_type', 'role', 'status', 'site', 'cluster', 'tenant',
|
||||
'vcpus', 'memory', 'disk', 'primary_ip',
|
||||
)
|
||||
|
||||
def render_disk(self, value):
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.models import Site
|
||||
from dcim.models import Platform, Site
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import ConfigTemplate, CustomField
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
@@ -167,54 +167,153 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
|
||||
]
|
||||
|
||||
|
||||
class VirtualMachineTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualMachineType
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
|
||||
user_permissions = ('dcim.view_platform', 'virtualization.view_virtualmachine')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.platforms = (
|
||||
Platform.objects.create(name='Platform 1', slug='platform-1'),
|
||||
Platform.objects.create(name='Platform 2', slug='platform-2'),
|
||||
Platform.objects.create(name='Platform 3', slug='platform-3'),
|
||||
)
|
||||
|
||||
cls.virtual_machine_types = (
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=cls.platforms[0],
|
||||
default_vcpus=1,
|
||||
default_memory=1024,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=cls.platforms[1],
|
||||
default_vcpus=2,
|
||||
default_memory=2048,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 3',
|
||||
slug='virtual-machine-type-3',
|
||||
default_platform=cls.platforms[2],
|
||||
default_vcpus=4,
|
||||
default_memory=4096,
|
||||
),
|
||||
)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Virtual Machine Type 4',
|
||||
'slug': 'virtual-machine-type-4',
|
||||
'default_platform': cls.platforms[0].pk,
|
||||
'default_vcpus': 1,
|
||||
'default_memory': 1024,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine Type 5',
|
||||
'slug': 'virtual-machine-type-5',
|
||||
'default_platform': cls.platforms[1].pk,
|
||||
'default_vcpus': 2,
|
||||
'default_memory': 2048,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine Type 6',
|
||||
'slug': 'virtual-machine-type-6',
|
||||
'default_platform': cls.platforms[2].pk,
|
||||
'default_vcpus': 4,
|
||||
'default_memory': 4096,
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'default_platform': cls.platforms[2].pk,
|
||||
'default_vcpus': 8,
|
||||
'default_memory': 8192,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualMachine
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'staged',
|
||||
}
|
||||
user_permissions = ('dcim.view_platform', 'virtualization.view_virtualmachinetype')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
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 = (
|
||||
cls.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)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
clusters = (
|
||||
Cluster(name='Cluster 1', type=clustertype, scope=sites[0], group=clustergroup),
|
||||
Cluster(name='Cluster 2', type=clustertype, scope=sites[1], group=clustergroup),
|
||||
cls.clusters = (
|
||||
Cluster(name='Cluster 1', type=clustertype, scope=cls.sites[0], group=clustergroup),
|
||||
Cluster(name='Cluster 2', type=clustertype, scope=cls.sites[1], group=clustergroup),
|
||||
Cluster(name='Cluster 3', type=clustertype),
|
||||
)
|
||||
for cluster in clusters:
|
||||
for cluster in cls.clusters:
|
||||
cluster.save()
|
||||
|
||||
device1 = create_test_device('device1', site=sites[0], cluster=clusters[0])
|
||||
device2 = create_test_device('device2', site=sites[1], cluster=clusters[1])
|
||||
cls.devices = (
|
||||
create_test_device('device1', site=cls.sites[0], cluster=cls.clusters[0]),
|
||||
create_test_device('device2', site=cls.sites[1], cluster=cls.clusters[1]),
|
||||
)
|
||||
|
||||
cls.platforms = (
|
||||
Platform.objects.create(name='Platform 1', slug='platform-1'),
|
||||
Platform.objects.create(name='Platform 2', slug='platform-2'),
|
||||
Platform.objects.create(name='Platform 3', slug='platform-3'),
|
||||
)
|
||||
|
||||
cls.vm_types = (
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=cls.platforms[0],
|
||||
default_vcpus=2,
|
||||
default_memory=4096,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=cls.platforms[1],
|
||||
default_vcpus=4,
|
||||
default_memory=8192,
|
||||
),
|
||||
)
|
||||
|
||||
virtual_machines = (
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 1',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=device1,
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
device=cls.devices[0],
|
||||
platform=cls.platforms[0],
|
||||
vcpus=2,
|
||||
memory=4096,
|
||||
local_context_data={'A': 1},
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 2',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
local_context_data={'B': 2
|
||||
}),
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
local_context_data={'B': 2},
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 3',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
local_context_data={'C': 3},
|
||||
start_on_boot=VirtualMachineStartOnBootChoices.STATUS_ON,
|
||||
),
|
||||
@@ -224,26 +323,89 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Virtual Machine 4',
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
'device': device2.pk,
|
||||
'site': cls.sites[1].pk,
|
||||
'cluster': cls.clusters[1].pk,
|
||||
'device': cls.devices[1].pk,
|
||||
'virtual_machine_type': cls.vm_types[0].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 5',
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
'site': cls.sites[1].pk,
|
||||
'cluster': cls.clusters[1].pk,
|
||||
'virtual_machine_type': cls.vm_types[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 6',
|
||||
'site': sites[1].pk,
|
||||
'site': cls.sites[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Machine 7',
|
||||
'cluster': clusters[2].pk,
|
||||
'cluster': cls.clusters[2].pk,
|
||||
'virtual_machine_type': cls.vm_types[0].pk,
|
||||
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON,
|
||||
},
|
||||
]
|
||||
|
||||
def test_virtual_machine_type_defaults_applied_on_create(self):
|
||||
data = {
|
||||
'name': 'Virtual Machine With Defaults',
|
||||
'site': self.sites[1].pk,
|
||||
'cluster': self.clusters[1].pk,
|
||||
'virtual_machine_type': self.vm_types[0].pk,
|
||||
'platform': None,
|
||||
'vcpus': None,
|
||||
'memory': None,
|
||||
}
|
||||
self.add_permissions('virtualization.add_virtualmachine')
|
||||
|
||||
response = self.client.post(self._get_list_url(), data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
vm = VirtualMachine.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[0])
|
||||
self.assertEqual(vm.platform, self.vm_types[0].default_platform)
|
||||
self.assertEqual(vm.vcpus, self.vm_types[0].default_vcpus)
|
||||
self.assertEqual(vm.memory, self.vm_types[0].default_memory)
|
||||
|
||||
def test_virtual_machine_type_defaults_do_not_override_explicit_values(self):
|
||||
data = {
|
||||
'name': 'Virtual Machine With Explicit Values',
|
||||
'site': self.sites[1].pk,
|
||||
'cluster': self.clusters[1].pk,
|
||||
'virtual_machine_type': self.vm_types[0].pk,
|
||||
'platform': self.platforms[2].pk,
|
||||
'vcpus': 6,
|
||||
'memory': 12288,
|
||||
}
|
||||
self.add_permissions('virtualization.add_virtualmachine')
|
||||
|
||||
response = self.client.post(self._get_list_url(), data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
vm = VirtualMachine.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[0])
|
||||
self.assertEqual(vm.platform, self.platforms[2])
|
||||
self.assertEqual(vm.vcpus, 6)
|
||||
self.assertEqual(vm.memory, 12288)
|
||||
|
||||
def test_setting_virtual_machine_type_on_existing_vm_does_not_backfill_defaults(self):
|
||||
vm = VirtualMachine.objects.get(name='Virtual Machine 2')
|
||||
self.add_permissions('virtualization.change_virtualmachine')
|
||||
|
||||
response = self.client.patch(
|
||||
self._get_detail_url(vm),
|
||||
{'virtual_machine_type': self.vm_types[1].pk},
|
||||
format='json',
|
||||
**self.header,
|
||||
)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
vm.refresh_from_db()
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[1])
|
||||
self.assertIsNone(vm.platform)
|
||||
self.assertIsNone(vm.vcpus)
|
||||
self.assertIsNone(vm.memory)
|
||||
|
||||
def test_config_context_included_by_default_in_list_view(self):
|
||||
"""
|
||||
Check that config context data is included by default in the virtual machines list.
|
||||
|
||||
@@ -230,6 +230,116 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualMachineTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
filterset = VirtualMachineTypeFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
Platform(name='Platform 3', slug='platform-3'),
|
||||
)
|
||||
for platform in platforms:
|
||||
platform.save()
|
||||
|
||||
cluster_type = ClusterType.objects.create(
|
||||
name='Cluster Type 1',
|
||||
slug='cluster-type-1',
|
||||
)
|
||||
site = Site.objects.create(
|
||||
name='Site 1',
|
||||
slug='site-1',
|
||||
)
|
||||
cluster = Cluster.objects.create(
|
||||
name='Cluster 1',
|
||||
type=cluster_type,
|
||||
scope=site,
|
||||
)
|
||||
|
||||
cls.vm_types = (
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=platforms[0],
|
||||
default_vcpus=1,
|
||||
default_memory=1024,
|
||||
description='foobar1',
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=platforms[1],
|
||||
default_vcpus=2,
|
||||
default_memory=2048,
|
||||
description='foobar2',
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 3',
|
||||
slug='virtual-machine-type-3',
|
||||
default_platform=platforms[2],
|
||||
default_vcpus=4,
|
||||
default_memory=4096,
|
||||
description='foobar3',
|
||||
),
|
||||
)
|
||||
|
||||
# Populate virtual_machine_count
|
||||
VirtualMachine.objects.create(
|
||||
name='vm-type-1a',
|
||||
cluster=cluster,
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
)
|
||||
VirtualMachine.objects.create(
|
||||
name='vm-type-1b',
|
||||
cluster=cluster,
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
tenant=Tenant.objects.create(name='Tenant 1', slug='tenant-1'),
|
||||
)
|
||||
VirtualMachine.objects.create(
|
||||
name='vm-type-2a',
|
||||
cluster=cluster,
|
||||
virtual_machine_type=cls.vm_types[1],
|
||||
)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Virtual Machine Type 1', 'Virtual Machine Type 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['virtual-machine-type-1', 'virtual-machine-type-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_default_platform(self):
|
||||
platforms = Platform.objects.all()[:2]
|
||||
params = {'default_platform_id': [platforms[0].pk, platforms[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'default_platform': [platforms[0].slug, platforms[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_default_vcpus(self):
|
||||
params = {'default_vcpus': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_default_memory(self):
|
||||
params = {'default_memory': [1024, 2048]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_machine_count(self):
|
||||
params = {'virtual_machine_count': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualMachine.objects.all()
|
||||
filterset = VirtualMachineFilterSet
|
||||
@@ -290,6 +400,30 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for platform in platforms:
|
||||
platform.save()
|
||||
|
||||
cls.vm_types = (
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=platforms[0],
|
||||
default_vcpus=1,
|
||||
default_memory=1024,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=platforms[1],
|
||||
default_vcpus=2,
|
||||
default_memory=2048,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='Virtual Machine Type 3',
|
||||
slug='virtual-machine-type-3',
|
||||
default_platform=platforms[2],
|
||||
default_vcpus=4,
|
||||
default_memory=4096,
|
||||
),
|
||||
)
|
||||
|
||||
roles = (
|
||||
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||
@@ -322,6 +456,7 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vms = (
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 1',
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=devices[0],
|
||||
@@ -333,11 +468,12 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
memory=1,
|
||||
disk=1,
|
||||
description='foobar1',
|
||||
local_context_data={"foo": 123},
|
||||
serial='111-aaa'
|
||||
local_context_data={'foo': 123},
|
||||
serial='111-aaa',
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 2',
|
||||
virtual_machine_type=cls.vm_types[1],
|
||||
site=sites[1],
|
||||
cluster=clusters[1],
|
||||
device=devices[1],
|
||||
@@ -354,6 +490,7 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 3',
|
||||
virtual_machine_type=cls.vm_types[2],
|
||||
site=sites[2],
|
||||
cluster=clusters[2],
|
||||
device=devices[2],
|
||||
@@ -499,6 +636,13 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'platform': [platforms[0].slug, platforms[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_machine_type(self):
|
||||
vm_types = VirtualMachineType.objects.all()[:2]
|
||||
params = {'virtual_machine_type_id': [vm_types[0].pk, vm_types[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'virtual_machine_type': [vm_types[0].slug, vm_types[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_mac_address(self):
|
||||
params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -1,12 +1,106 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Site
|
||||
from dcim.models import Platform, Site
|
||||
from tenancy.models import Tenant
|
||||
from utilities.testing import create_test_device
|
||||
from virtualization.models import *
|
||||
|
||||
|
||||
class VirtualMachineTypeTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.platform = Platform.objects.create(
|
||||
name='Type Test Ubuntu 24.04',
|
||||
slug='type-test-ubuntu-24-04',
|
||||
)
|
||||
cls.virtual_machine_type = VirtualMachineType.objects.create(
|
||||
name='Small Linux',
|
||||
slug='small-linux',
|
||||
default_platform=cls.platform,
|
||||
default_vcpus=Decimal('2.00'),
|
||||
default_memory=4096,
|
||||
)
|
||||
|
||||
cls.cluster_type = ClusterType.objects.create(
|
||||
name='VM Type Count Cluster Type',
|
||||
slug='vm-type-count-cluster-type',
|
||||
)
|
||||
cls.site = Site.objects.create(
|
||||
name='VM Type Count Site',
|
||||
slug='vm-type-count-site',
|
||||
)
|
||||
cls.cluster = Cluster.objects.create(
|
||||
name='VM Type Count Cluster',
|
||||
type=cls.cluster_type,
|
||||
scope=cls.site,
|
||||
)
|
||||
|
||||
def test_virtual_machine_type_str_and_defaults(self):
|
||||
"""
|
||||
Verify that the string representation of a VirtualMachineType returns
|
||||
its name, and that all default fields (platform, vcpus, memory) are
|
||||
stored correctly after creation.
|
||||
"""
|
||||
self.assertEqual(str(self.virtual_machine_type), 'Small Linux')
|
||||
self.assertEqual(self.virtual_machine_type.default_platform, self.platform)
|
||||
self.assertEqual(self.virtual_machine_type.default_vcpus, Decimal('2.00'))
|
||||
self.assertEqual(self.virtual_machine_type.default_memory, 4096)
|
||||
|
||||
def test_virtual_machine_type_virtual_machine_count(self):
|
||||
"""
|
||||
The virtual_machine_count counter cache field should accurately track
|
||||
the number of VirtualMachines referencing this type through creation,
|
||||
additional insertions, reassignment, and deletion.
|
||||
"""
|
||||
# Starts at zero
|
||||
self.assertEqual(self.virtual_machine_type.virtual_machine_count, 0)
|
||||
|
||||
# Create the first VM
|
||||
vm1 = VirtualMachine.objects.create(
|
||||
name='vm-count-test-1',
|
||||
cluster=self.cluster,
|
||||
virtual_machine_type=self.virtual_machine_type,
|
||||
)
|
||||
self.virtual_machine_type.refresh_from_db()
|
||||
self.assertEqual(self.virtual_machine_type.virtual_machine_count, 1)
|
||||
|
||||
# Create the second VM
|
||||
vm2 = VirtualMachine.objects.create(
|
||||
name='vm-count-test-2',
|
||||
cluster=self.cluster,
|
||||
virtual_machine_type=self.virtual_machine_type,
|
||||
)
|
||||
self.virtual_machine_type.refresh_from_db()
|
||||
self.assertEqual(self.virtual_machine_type.virtual_machine_count, 2)
|
||||
|
||||
# Delete one VM — count should decrement
|
||||
vm1.delete()
|
||||
self.virtual_machine_type.refresh_from_db()
|
||||
self.assertEqual(self.virtual_machine_type.virtual_machine_count, 1)
|
||||
|
||||
# Reassign the remaining VM to no type — count should drop to zero
|
||||
vm2.virtual_machine_type = None
|
||||
vm2.save()
|
||||
self.virtual_machine_type.refresh_from_db()
|
||||
self.assertEqual(self.virtual_machine_type.virtual_machine_count, 0)
|
||||
|
||||
def test_virtual_machine_type_invalid_default_vcpus(self):
|
||||
"""
|
||||
default_vcpus below the minimum should fail validation.
|
||||
"""
|
||||
vmt = VirtualMachineType(
|
||||
name='Zero vCPU Type',
|
||||
slug='zero-vcpu-type',
|
||||
default_vcpus=Decimal('0.00'),
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
vmt.full_clean()
|
||||
|
||||
|
||||
class VirtualMachineTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
@@ -14,6 +108,12 @@ class VirtualMachineTestCase(TestCase):
|
||||
# Create the cluster type
|
||||
cls.cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
|
||||
# Create platforms
|
||||
cls.platforms = (
|
||||
Platform.objects.create(name='VM Default Ubuntu 24.04', slug='vm-default-ubuntu-24-04'),
|
||||
Platform.objects.create(name='VM Default Debian 12', slug='vm-default-debian-12'),
|
||||
)
|
||||
|
||||
# Create sites
|
||||
cls.sites = (
|
||||
Site.objects.create(name='Site 1', slug='site-1'),
|
||||
@@ -59,6 +159,24 @@ class VirtualMachineTestCase(TestCase):
|
||||
Tenant.objects.create(name='Tenant 2', slug='tenant-2'),
|
||||
)
|
||||
|
||||
# Create virtual machine types
|
||||
cls.vm_types = (
|
||||
VirtualMachineType.objects.create(
|
||||
name='General Purpose Small',
|
||||
slug='general-purpose-small',
|
||||
default_platform=cls.platforms[0],
|
||||
default_vcpus=Decimal('2.00'),
|
||||
default_memory=4096,
|
||||
),
|
||||
VirtualMachineType.objects.create(
|
||||
name='General Purpose Large',
|
||||
slug='general-purpose-large',
|
||||
default_platform=cls.platforms[1],
|
||||
default_vcpus=Decimal('8.00'),
|
||||
default_memory=16384,
|
||||
),
|
||||
)
|
||||
|
||||
def test_vm_duplicate_name_per_cluster(self):
|
||||
"""
|
||||
Test that creating two Virtual Machines with the same name in
|
||||
@@ -157,6 +275,181 @@ class VirtualMachineTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
vm.full_clean()
|
||||
|
||||
#
|
||||
# Virtual Machine Type tests
|
||||
#
|
||||
|
||||
def test_vm_type_defaults_applied_on_create(self):
|
||||
"""
|
||||
When a new VirtualMachine is created with a VirtualMachineType and no
|
||||
explicit platform, vcpus, or memory, the type's defaults should be
|
||||
automatically applied via apply_type_defaults().
|
||||
"""
|
||||
vm = VirtualMachine(
|
||||
name='vm-type-defaults',
|
||||
cluster=self.cluster_with_site,
|
||||
virtual_machine_type=self.vm_types[0],
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.platform, self.platforms[0])
|
||||
self.assertEqual(vm.vcpus, Decimal('2.00'))
|
||||
self.assertEqual(vm.memory, 4096)
|
||||
|
||||
def test_vm_type_defaults_do_not_override_explicit_values(self):
|
||||
"""
|
||||
When a new VirtualMachine specifies explicit values for a platform,
|
||||
vcpus, and memory, those values must be preserved even if the
|
||||
assigned VirtualMachineType defines different defaults.
|
||||
"""
|
||||
vm = VirtualMachine(
|
||||
name='vm-type-explicit',
|
||||
cluster=self.cluster_with_site,
|
||||
virtual_machine_type=self.vm_types[0],
|
||||
platform=self.platforms[1],
|
||||
vcpus=Decimal('4.00'),
|
||||
memory=8192,
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.platform, self.platforms[1])
|
||||
self.assertEqual(vm.vcpus, Decimal('4.00'))
|
||||
self.assertEqual(vm.memory, 8192)
|
||||
|
||||
def test_vm_type_added_to_existing_vm_does_not_backfill_defaults(self):
|
||||
"""
|
||||
Assigning a VirtualMachineType to an already-saved VirtualMachine
|
||||
(i.e. an update, not a creation) must not retroactively populate
|
||||
the VM's fields with the type's defaults, since apply_type_defaults()
|
||||
only runs on initial creation.
|
||||
"""
|
||||
vm = VirtualMachine(
|
||||
name='vm-type-added-later',
|
||||
cluster=self.cluster_with_site,
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
|
||||
vm.virtual_machine_type = self.vm_types[0]
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[0])
|
||||
self.assertIsNone(vm.platform)
|
||||
self.assertIsNone(vm.vcpus)
|
||||
self.assertIsNone(vm.memory)
|
||||
|
||||
def test_vm_type_change_does_not_overwrite_existing_values(self):
|
||||
"""
|
||||
Changing the VirtualMachineType on an existing VirtualMachine must
|
||||
not overwrite field values that were previously set — either
|
||||
explicitly or via earlier type defaults.
|
||||
"""
|
||||
vm = VirtualMachine(
|
||||
name='vm-type-change',
|
||||
cluster=self.cluster_with_site,
|
||||
virtual_machine_type=self.vm_types[0],
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.platform, self.platforms[0])
|
||||
self.assertEqual(vm.vcpus, Decimal('2.00'))
|
||||
self.assertEqual(vm.memory, 4096)
|
||||
|
||||
vm.platform = self.platforms[1]
|
||||
vm.vcpus = Decimal('6.00')
|
||||
vm.memory = 12288
|
||||
vm.virtual_machine_type = self.vm_types[1]
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.platform, self.platforms[1])
|
||||
self.assertEqual(vm.vcpus, Decimal('6.00'))
|
||||
self.assertEqual(vm.memory, 12288)
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[1])
|
||||
|
||||
def test_vm_type_partial_defaults(self):
|
||||
"""
|
||||
A VirtualMachineType with only some defaults set should only populate
|
||||
those fields on a new VM, leaving the rest as None.
|
||||
"""
|
||||
partial_type = VirtualMachineType.objects.create(
|
||||
name='Partial Defaults',
|
||||
slug='partial-defaults',
|
||||
default_vcpus=Decimal('4.00'),
|
||||
# default_platform and default_memory intentionally left None
|
||||
)
|
||||
|
||||
vm = VirtualMachine(
|
||||
name='vm-partial-defaults',
|
||||
cluster=self.cluster_with_site,
|
||||
virtual_machine_type=partial_type,
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertIsNone(vm.platform)
|
||||
self.assertEqual(vm.vcpus, Decimal('4.00'))
|
||||
self.assertIsNone(vm.memory)
|
||||
|
||||
def test_vm_type_no_defaults(self):
|
||||
"""
|
||||
A VirtualMachineType with all default fields as None should not
|
||||
alter any VM fields on creation.
|
||||
"""
|
||||
empty_type = VirtualMachineType.objects.create(
|
||||
name='Empty Type',
|
||||
slug='empty-type',
|
||||
)
|
||||
|
||||
vm = VirtualMachine(
|
||||
name='vm-empty-type',
|
||||
cluster=self.cluster_with_site,
|
||||
virtual_machine_type=empty_type,
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertEqual(vm.virtual_machine_type, empty_type)
|
||||
self.assertIsNone(vm.platform)
|
||||
self.assertIsNone(vm.vcpus)
|
||||
self.assertIsNone(vm.memory)
|
||||
|
||||
def test_vm_created_without_type(self):
|
||||
"""
|
||||
A VM created without a VirtualMachineType should not raise any errors
|
||||
in apply_type_defaults() and should leave all fields as None.
|
||||
"""
|
||||
vm = VirtualMachine(
|
||||
name='vm-no-type',
|
||||
cluster=self.cluster_with_site,
|
||||
)
|
||||
vm.full_clean()
|
||||
vm.save()
|
||||
vm.refresh_from_db()
|
||||
|
||||
self.assertIsNone(vm.virtual_machine_type)
|
||||
self.assertIsNone(vm.platform)
|
||||
self.assertIsNone(vm.vcpus)
|
||||
self.assertIsNone(vm.memory)
|
||||
|
||||
def test_vm_type_is_included_in_clone_fields(self):
|
||||
"""
|
||||
Verify that virtual_machine_type is part of clone_fields so it
|
||||
carries over when cloning a VM.
|
||||
"""
|
||||
self.assertIn('virtual_machine_type', VirtualMachine.clone_fields)
|
||||
|
||||
#
|
||||
# Device assignment tests
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
@@ -202,6 +204,81 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
|
||||
|
||||
class VirtualMachineTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualMachineType
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
cls.platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
Platform(name='Platform 3', slug='platform-3'),
|
||||
)
|
||||
for platform in cls.platforms:
|
||||
platform.save()
|
||||
|
||||
cls.virtual_machine_types = (
|
||||
VirtualMachineType(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=cls.platforms[0],
|
||||
default_vcpus=Decimal('1.00'),
|
||||
default_memory=1024,
|
||||
),
|
||||
VirtualMachineType(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=cls.platforms[1],
|
||||
default_vcpus=Decimal('2.00'),
|
||||
default_memory=2048,
|
||||
),
|
||||
VirtualMachineType(
|
||||
name='Virtual Machine Type 3',
|
||||
slug='virtual-machine-type-3',
|
||||
default_platform=cls.platforms[2],
|
||||
default_vcpus=Decimal('4.00'),
|
||||
default_memory=4096,
|
||||
),
|
||||
)
|
||||
for virtual_machine_type in cls.virtual_machine_types:
|
||||
virtual_machine_type.save()
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Virtual Machine Type X',
|
||||
'slug': 'virtual-machine-type-x',
|
||||
'default_platform': cls.platforms[1].pk,
|
||||
'default_vcpus': 8,
|
||||
'default_memory': 8192,
|
||||
'description': 'A new virtual machine type',
|
||||
'comments': 'Some comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
'name,slug,default_platform,default_vcpus,default_memory,description',
|
||||
'Virtual Machine Type 4,virtual-machine-type-4,Platform 1,1.00,1024,Fourth virtual machine type',
|
||||
'Virtual Machine Type 5,virtual-machine-type-5,Platform 2,2.00,2048,Fifth virtual machine type',
|
||||
'Virtual Machine Type 6,virtual-machine-type-6,Platform 3,4.00,4096,Sixth virtual machine type',
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
'id,name,description',
|
||||
f'{cls.virtual_machine_types[0].pk},Virtual Machine Type 7,New description 7',
|
||||
f'{cls.virtual_machine_types[1].pk},Virtual Machine Type 8,New description 8',
|
||||
f'{cls.virtual_machine_types[2].pk},Virtual Machine Type 9,New description 9',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'default_platform': cls.platforms[2].pk,
|
||||
'default_vcpus': 16,
|
||||
'default_memory': 16384,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualMachine
|
||||
|
||||
@@ -215,57 +292,79 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
for role in roles:
|
||||
role.save()
|
||||
|
||||
platforms = (
|
||||
cls.platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
)
|
||||
for platform in platforms:
|
||||
for platform in cls.platforms:
|
||||
platform.save()
|
||||
|
||||
sites = (
|
||||
cls.sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
|
||||
clusters = (
|
||||
Cluster(name='Cluster 1', type=clustertype, scope=sites[0]),
|
||||
Cluster(name='Cluster 2', type=clustertype, scope=sites[1]),
|
||||
cls.clusters = (
|
||||
Cluster(name='Cluster 1', type=clustertype, scope=cls.sites[0]),
|
||||
Cluster(name='Cluster 2', type=clustertype, scope=cls.sites[1]),
|
||||
)
|
||||
for cluster in clusters:
|
||||
for cluster in cls.clusters:
|
||||
cluster.save()
|
||||
|
||||
devices = (
|
||||
create_test_device('device1', site=sites[0], cluster=clusters[0]),
|
||||
create_test_device('device2', site=sites[1], cluster=clusters[1]),
|
||||
cls.devices = (
|
||||
create_test_device('device1', site=cls.sites[0], cluster=cls.clusters[0]),
|
||||
create_test_device('device2', site=cls.sites[1], cluster=cls.clusters[1]),
|
||||
)
|
||||
|
||||
cls.vm_types = (
|
||||
VirtualMachineType(
|
||||
name='Virtual Machine Type 1',
|
||||
slug='virtual-machine-type-1',
|
||||
default_platform=cls.platforms[0],
|
||||
default_vcpus=Decimal('2.00'),
|
||||
default_memory=4096,
|
||||
),
|
||||
VirtualMachineType(
|
||||
name='Virtual Machine Type 2',
|
||||
slug='virtual-machine-type-2',
|
||||
default_platform=cls.platforms[1],
|
||||
default_vcpus=Decimal('4.00'),
|
||||
default_memory=8192,
|
||||
),
|
||||
)
|
||||
for vm_type in cls.vm_types:
|
||||
vm_type.save()
|
||||
|
||||
virtual_machines = (
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 1',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=devices[0],
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
device=cls.devices[0],
|
||||
role=roles[0],
|
||||
platform=platforms[0],
|
||||
platform=cls.platforms[0],
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 2',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=devices[0],
|
||||
virtual_machine_type=cls.vm_types[0],
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
device=cls.devices[0],
|
||||
role=roles[0],
|
||||
platform=platforms[0],
|
||||
platform=cls.platforms[0],
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 3',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=devices[0],
|
||||
virtual_machine_type=cls.vm_types[1],
|
||||
site=cls.sites[0],
|
||||
cluster=cls.clusters[0],
|
||||
device=cls.devices[0],
|
||||
role=roles[0],
|
||||
platform=platforms[0],
|
||||
platform=cls.platforms[0],
|
||||
),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
@@ -273,11 +372,12 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'cluster': clusters[1].pk,
|
||||
'device': devices[1].pk,
|
||||
'site': sites[1].pk,
|
||||
'virtual_machine_type': cls.vm_types[1].pk,
|
||||
'cluster': cls.clusters[1].pk,
|
||||
'device': cls.devices[1].pk,
|
||||
'site': cls.sites[1].pk,
|
||||
'tenant': None,
|
||||
'platform': platforms[1].pk,
|
||||
'platform': cls.platforms[1].pk,
|
||||
'name': 'Virtual Machine X',
|
||||
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
||||
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON,
|
||||
@@ -294,34 +394,61 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"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,",
|
||||
'name,status,site,cluster,device,virtual_machine_type',
|
||||
'Virtual Machine 4,active,Site 1,Cluster 1,device1,Virtual Machine Type 1',
|
||||
'Virtual Machine 5,active,Site 1,Cluster 1,device1,Virtual Machine Type 2',
|
||||
'Virtual Machine 6,active,Site 1,Cluster 1,,Virtual Machine Type 1',
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,comments",
|
||||
f"{virtual_machines[0].pk},Virtual Machine 7,New comments 7",
|
||||
f"{virtual_machines[1].pk},Virtual Machine 8,New comments 8",
|
||||
f"{virtual_machines[2].pk},Virtual Machine 9,New comments 9",
|
||||
'id,name,comments',
|
||||
f'{virtual_machines[0].pk},Virtual Machine 7,New comments 7',
|
||||
f'{virtual_machines[1].pk},Virtual Machine 8,New comments 8',
|
||||
f'{virtual_machines[2].pk},Virtual Machine 9,New comments 9',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'cluster': clusters[1].pk,
|
||||
'device': devices[1].pk,
|
||||
'virtual_machine_type': cls.vm_types[1].pk,
|
||||
'site': cls.sites[1].pk,
|
||||
'cluster': cls.clusters[1].pk,
|
||||
'device': cls.devices[1].pk,
|
||||
'tenant': None,
|
||||
'platform': platforms[1].pk,
|
||||
'platform': cls.platforms[1].pk,
|
||||
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
||||
'role': roles[1].pk,
|
||||
'vcpus': 8,
|
||||
'vcpus': Decimal('8.00'),
|
||||
'memory': 65535,
|
||||
'disk': 8000,
|
||||
'comments': 'New comments',
|
||||
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_OFF,
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_create_virtualmachine_with_type_defaults(self):
|
||||
self.add_permissions('virtualization.add_virtualmachine')
|
||||
|
||||
response = self.client.post(
|
||||
self._get_url('add'),
|
||||
data={
|
||||
'name': 'Virtual Machine Defaults',
|
||||
'virtual_machine_type': self.vm_types[0].pk,
|
||||
'status': VirtualMachineStatusChoices.STATUS_ACTIVE,
|
||||
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_OFF,
|
||||
'site': self.sites[0].pk,
|
||||
'cluster': self.clusters[0].pk,
|
||||
'platform': '',
|
||||
'vcpus': '',
|
||||
'memory': '',
|
||||
},
|
||||
)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
vm = VirtualMachine.objects.get(name='Virtual Machine Defaults')
|
||||
self.assertEqual(vm.virtual_machine_type, self.vm_types[0])
|
||||
self.assertEqual(vm.platform, self.platforms[0])
|
||||
self.assertEqual(vm.vcpus, self.vm_types[0].default_vcpus)
|
||||
self.assertEqual(vm.memory, self.vm_types[0].default_memory)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_virtualmachine_interfaces(self):
|
||||
virtualmachine = VirtualMachine.objects.first()
|
||||
|
||||
@@ -2,6 +2,10 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs, panels
|
||||
|
||||
#
|
||||
# Cluster
|
||||
#
|
||||
|
||||
|
||||
class ClusterPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
@@ -13,8 +17,27 @@ class ClusterPanel(panels.ObjectAttributesPanel):
|
||||
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
|
||||
|
||||
|
||||
#
|
||||
# Virtual machine types
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachineTypePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
default_platform = attrs.RelatedObjectAttr('default_platform', linkify=True)
|
||||
default_vcpus = attrs.TextAttr('default_vcpus', label=_('Default vCPUs'))
|
||||
default_memory = attrs.TextAttr('default_memory', format_string=_('{} MB'), label=_('Default memory'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
|
||||
class VirtualMachinePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
virtual_machine_type = attrs.RelatedObjectAttr('virtual_machine_type', linkify=True, label=_('Type'))
|
||||
status = attrs.ChoiceAttr('status')
|
||||
start_on_boot = attrs.ChoiceAttr('start_on_boot')
|
||||
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||
@@ -44,6 +67,11 @@ class VirtualMachinePlacementPanel(panels.ObjectAttributesPanel):
|
||||
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||
|
||||
|
||||
#
|
||||
# Virtual disks
|
||||
#
|
||||
|
||||
|
||||
class VirtualDiskPanel(panels.ObjectAttributesPanel):
|
||||
virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine'))
|
||||
name = attrs.TextAttr('name')
|
||||
@@ -51,6 +79,11 @@ class VirtualDiskPanel(panels.ObjectAttributesPanel):
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
#
|
||||
# VM interfaces
|
||||
#
|
||||
|
||||
|
||||
class VMInterfacePanel(panels.ObjectAttributesPanel):
|
||||
virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine'))
|
||||
name = attrs.TextAttr('name')
|
||||
|
||||
@@ -16,6 +16,9 @@ urlpatterns = [
|
||||
path('clusters/', include(get_model_urls('virtualization', 'cluster', detail=False))),
|
||||
path('clusters/<int:pk>/', include(get_model_urls('virtualization', 'cluster'))),
|
||||
|
||||
path('virtual-machine-types/', include(get_model_urls('virtualization', 'virtualmachinetype', detail=False))),
|
||||
path('virtual-machine-types/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachinetype'))),
|
||||
|
||||
path('virtual-machines/', include(get_model_urls('virtualization', 'virtualmachine', detail=False))),
|
||||
path('virtual-machines/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachine'))),
|
||||
|
||||
|
||||
@@ -387,6 +387,80 @@ class ClusterAddDevicesView(generic.ObjectEditView):
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Virtual machine types
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'list', path='', detail=False)
|
||||
class VirtualMachineTypeListView(generic.ObjectListView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
filterset = filtersets.VirtualMachineTypeFilterSet
|
||||
filterset_form = forms.VirtualMachineTypeFilterForm
|
||||
table = tables.VirtualMachineTypeTable
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType)
|
||||
class VirtualMachineTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VirtualMachineTypePanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'add', detail=False)
|
||||
@register_model_view(VirtualMachineType, 'edit')
|
||||
class VirtualMachineTypeEditView(generic.ObjectEditView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
form = forms.VirtualMachineTypeForm
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'delete')
|
||||
class VirtualMachineTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'bulk_import', path='import', detail=False)
|
||||
class VirtualMachineTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
model_form = forms.VirtualMachineTypeImportForm
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'bulk_edit', path='edit', detail=False)
|
||||
class VirtualMachineTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
filterset = filtersets.VirtualMachineTypeFilterSet
|
||||
table = tables.VirtualMachineTypeTable
|
||||
form = forms.VirtualMachineTypeBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'bulk_rename', path='rename', detail=False)
|
||||
class VirtualMachineTypeBulkRenameView(generic.BulkRenameView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
filterset = filtersets.VirtualMachineTypeFilterSet
|
||||
|
||||
|
||||
@register_model_view(VirtualMachineType, 'bulk_delete', path='delete', detail=False)
|
||||
class VirtualMachineTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualMachineType.objects.all()
|
||||
filterset = filtersets.VirtualMachineTypeFilterSet
|
||||
table = tables.VirtualMachineTypeTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user