mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into 2921-tags-select2
This commit is contained in:
@@ -3,14 +3,14 @@ from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||
from dcim.constants import IFACE_TYPE_CHOICES, IFACE_TYPE_VIRTUAL, IFACE_MODE_CHOICES
|
||||
from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
|
||||
from virtualization.constants import VM_STATUS_CHOICES
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -38,6 +38,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
|
||||
class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
type = NestedClusterTypeSerializer()
|
||||
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@@ -46,7 +47,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = [
|
||||
'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
@@ -56,7 +57,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
#
|
||||
|
||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||
site = NestedSiteSerializer(read_only=True)
|
||||
cluster = NestedClusterSerializer()
|
||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||
@@ -74,6 +75,7 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
|
||||
class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
@@ -97,8 +99,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
type = ChoiceField(choices=IFACE_TYPE_CHOICES, default=IFACE_TYPE_VIRTUAL, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
|
@@ -15,7 +15,8 @@ from . import serializers
|
||||
|
||||
class VirtualizationFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(VirtualMachine, ['status']),
|
||||
(serializers.VirtualMachineSerializer, ['status']),
|
||||
(serializers.InterfaceSerializer, ['type']),
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +29,7 @@ class ClusterTypeViewSet(ModelViewSet):
|
||||
cluster_count=Count('clusters')
|
||||
)
|
||||
serializer_class = serializers.ClusterTypeSerializer
|
||||
filterset_class = filters.ClusterTypeFilter
|
||||
filterset_class = filters.ClusterTypeFilterSet
|
||||
|
||||
|
||||
class ClusterGroupViewSet(ModelViewSet):
|
||||
@@ -36,18 +37,18 @@ class ClusterGroupViewSet(ModelViewSet):
|
||||
cluster_count=Count('clusters')
|
||||
)
|
||||
serializer_class = serializers.ClusterGroupSerializer
|
||||
filterset_class = filters.ClusterGroupFilter
|
||||
filterset_class = filters.ClusterGroupFilterSet
|
||||
|
||||
|
||||
class ClusterViewSet(CustomFieldModelViewSet):
|
||||
queryset = Cluster.objects.prefetch_related(
|
||||
'type', 'group', 'site', 'tags'
|
||||
'type', 'group', 'tenant', 'site', 'tags'
|
||||
).annotate(
|
||||
device_count=get_subquery(Device, 'cluster'),
|
||||
virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
|
||||
)
|
||||
serializer_class = serializers.ClusterSerializer
|
||||
filterset_class = filters.ClusterFilter
|
||||
filterset_class = filters.ClusterFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -58,7 +59,7 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
||||
queryset = VirtualMachine.objects.prefetch_related(
|
||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||
)
|
||||
filterset_class = filters.VirtualMachineFilter
|
||||
filterset_class = filters.VirtualMachineFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
@@ -88,7 +89,7 @@ class InterfaceViewSet(ModelViewSet):
|
||||
'virtual_machine', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filters.InterfaceFilter
|
||||
filterset_class = filters.InterfaceFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
request = self.get_serializer_context()['request']
|
||||
|
38
netbox/virtualization/choices.py
Normal file
38
netbox/virtualization/choices.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# VirtualMachines
|
||||
#
|
||||
|
||||
class VirtualMachineStatusChoices(ChoiceSet):
|
||||
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_STAGED = 'staged'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_OFFLINE, 'Offline'),
|
||||
(STATUS_STAGED, 'Staged'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
STATUS_OFFLINE: 0,
|
||||
STATUS_ACTIVE: 1,
|
||||
STATUS_STAGED: 3,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Interface types (for VirtualMachines)
|
||||
#
|
||||
|
||||
class VMInterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
TYPE_VIRTUAL = InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
|
||||
CHOICES = (
|
||||
(TYPE_VIRTUAL, 'Virtual'),
|
||||
)
|
@@ -1,15 +0,0 @@
|
||||
from dcim.constants import DEVICE_STATUS_ACTIVE, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_STAGED
|
||||
|
||||
# VirtualMachine statuses (replicated from Device statuses)
|
||||
VM_STATUS_CHOICES = [
|
||||
[DEVICE_STATUS_ACTIVE, 'Active'],
|
||||
[DEVICE_STATUS_OFFLINE, 'Offline'],
|
||||
[DEVICE_STATUS_STAGED, 'Staged'],
|
||||
]
|
||||
|
||||
# Bootstrap CSS classes for VirtualMachine statuses
|
||||
VM_STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
3: 'primary',
|
||||
}
|
@@ -2,39 +2,39 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilter
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import (
|
||||
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from .constants import *
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ClusterFilter',
|
||||
'ClusterGroupFilter',
|
||||
'ClusterTypeFilter',
|
||||
'InterfaceFilter',
|
||||
'VirtualMachineFilter',
|
||||
'ClusterFilterSet',
|
||||
'ClusterGroupFilterSet',
|
||||
'ClusterTypeFilterSet',
|
||||
'InterfaceFilterSet',
|
||||
'VirtualMachineFilterSet',
|
||||
)
|
||||
|
||||
|
||||
class ClusterTypeFilter(NameSlugSearchFilterSet):
|
||||
class ClusterTypeFilterSet(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ClusterGroupFilter(NameSlugSearchFilterSet):
|
||||
class ClusterGroupFilterSet(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -84,6 +84,10 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Cluster type (slug)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label="Tenant (ID)"
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
@@ -99,7 +103,12 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
class VirtualMachineFilterSet(
|
||||
LocalConfigContextFilterSet,
|
||||
TenancyFilterSet,
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -109,7 +118,7 @@ class VirtualMachineFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFie
|
||||
label='Search',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VM_STATUS_CHOICES,
|
||||
choices=VirtualMachineStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -199,7 +208,7 @@ class VirtualMachineFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFie
|
||||
)
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
class InterfaceFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
@@ -1,91 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Public Cloud",
|
||||
"slug": "public-cloud"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "vSphere",
|
||||
"slug": "vsphere"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Hyper-V",
|
||||
"slug": "hyper-v"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "libvirt",
|
||||
"slug": "libvirt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "LXD",
|
||||
"slug": "lxd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustertype",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Docker",
|
||||
"slug": "docker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.clustergroup",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "VM Host",
|
||||
"slug": "vm-host"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.cluster",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Digital Ocean",
|
||||
"type": 1,
|
||||
"group": 1,
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:42.289Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.cluster",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Amazon EC2",
|
||||
"type": 1,
|
||||
"group": 1,
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:42.289Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "virtualization.cluster",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Microsoft Azure",
|
||||
"type": 1,
|
||||
"group": 1,
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:42.289Z"
|
||||
}
|
||||
}
|
||||
]
|
@@ -2,7 +2,7 @@ from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL, IFACE_MODE_CHOICES
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
||||
@@ -15,13 +15,9 @@ from utilities.forms import (
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
|
||||
SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField
|
||||
)
|
||||
from .constants import *
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
VIFACE_TYPE_CHOICES = (
|
||||
(IFACE_TYPE_VIRTUAL, 'Virtual'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Cluster types
|
||||
@@ -77,7 +73,7 @@ class ClusterGroupCSVForm(forms.ModelForm):
|
||||
# Clusters
|
||||
#
|
||||
|
||||
class ClusterForm(BootstrapMixin, CustomFieldForm):
|
||||
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
required=False
|
||||
@@ -86,7 +82,7 @@ class ClusterForm(BootstrapMixin, CustomFieldForm):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = [
|
||||
'name', 'type', 'group', 'site', 'comments', 'tags',
|
||||
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'type': APISelect(
|
||||
@@ -128,6 +124,15 @@ class ClusterCSVForm(forms.ModelForm):
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid tenant name'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
@@ -153,6 +158,10 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
api_url="/api/virtualization/cluster-groups/"
|
||||
)
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
@@ -166,13 +175,25 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'group', 'site', 'comments',
|
||||
'group', 'site', 'comments', 'tenant',
|
||||
]
|
||||
|
||||
|
||||
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Cluster
|
||||
field_order = [
|
||||
'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
|
||||
]
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
type = FilterChoiceField(
|
||||
queryset=ClusterType.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-types/",
|
||||
value_field='slug',
|
||||
)
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
@@ -196,15 +217,6 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
type = FilterChoiceField(
|
||||
queryset=ClusterType.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-types/",
|
||||
value_field='slug',
|
||||
)
|
||||
)
|
||||
group = FilterChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
@@ -419,7 +431,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class VirtualMachineCSVForm(forms.ModelForm):
|
||||
status = CSVChoiceField(
|
||||
choices=VM_STATUS_CHOICES,
|
||||
choices=VirtualMachineStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status of device'
|
||||
)
|
||||
@@ -472,7 +484,7 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(VM_STATUS_CHOICES),
|
||||
choices=add_blank_choice(VirtualMachineStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2(),
|
||||
@@ -605,7 +617,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=VM_STATUS_CHOICES,
|
||||
choices=VirtualMachineStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
@@ -711,13 +723,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
tagged_vlans = self.cleaned_data['tagged_vlans']
|
||||
|
||||
# Untagged interfaces cannot be assigned tagged VLANs
|
||||
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
|
||||
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
|
||||
raise forms.ValidationError({
|
||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||
})
|
||||
|
||||
# Remove all tagged VLAN assignments from "tagged all" interfaces
|
||||
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
|
||||
self.cleaned_data['tagged_vlans'] = []
|
||||
|
||||
|
||||
@@ -726,8 +738,8 @@ class InterfaceCreateForm(ComponentForm):
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=VIFACE_TYPE_CHOICES,
|
||||
initial=IFACE_TYPE_VIRTUAL,
|
||||
choices=VMInterfaceTypeChoices,
|
||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
@@ -748,7 +760,7 @@ class InterfaceCreateForm(ComponentForm):
|
||||
required=False
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(IFACE_MODE_CHOICES),
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
@@ -833,7 +845,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
required=False
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(IFACE_MODE_CHOICES),
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
@@ -911,8 +923,8 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
|
||||
|
||||
class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=VIFACE_TYPE_CHOICES,
|
||||
initial=IFACE_TYPE_VIRTUAL,
|
||||
choices=VMInterfaceTypeChoices,
|
||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
|
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:23
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('virtualization', '0002_virtualmachine_add_status'), ('virtualization', '0003_cluster_add_site'), ('virtualization', '0004_virtualmachine_add_role')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0044_virtualization'),
|
||||
('virtualization', '0001_virtualization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [3, 'Staged']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.DeviceRole'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,89 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('virtualization', '0002_virtualmachine_add_status'), ('virtualization', '0003_cluster_add_site'), ('virtualization', '0004_virtualmachine_add_role'), ('virtualization', '0005_django2'), ('virtualization', '0006_tags'), ('virtualization', '0007_change_logging'), ('virtualization', '0008_virtualmachine_local_context_data'), ('virtualization', '0009_custom_tag_models')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0044_virtualization'),
|
||||
('virtualization', '0001_virtualization'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [3, 'Staged']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, limit_choices_to={'vm_role': True}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.DeviceRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clustergroup',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clustergroup',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clustertype',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clustertype',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cluster',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cluster',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='local_context_data',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualmachine',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
18
netbox/virtualization/migrations/0010_cluster_add_tenant.py
Normal file
18
netbox/virtualization/migrations/0010_cluster_add_tenant.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('virtualization', '0009_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,50 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
VIRTUALMACHINE_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(3, 'staged'),
|
||||
)
|
||||
|
||||
|
||||
def virtualmachine_status_to_slug(apps, schema_editor):
|
||||
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
|
||||
for id, slug in VIRTUALMACHINE_STATUS_CHOICES:
|
||||
VirtualMachine.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('virtualization', '0010_cluster_add_tenant'), ('virtualization', '0011_3569_virtualmachine_fields'), ('virtualization', '0012_vm_name_nonunique')]
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
('virtualization', '0009_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='virtualmachine',
|
||||
unique_together={('cluster', 'tenant', 'name')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=virtualmachine_status_to_slug,
|
||||
),
|
||||
]
|
@@ -0,0 +1,36 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
VIRTUALMACHINE_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(3, 'staged'),
|
||||
)
|
||||
|
||||
|
||||
def virtualmachine_status_to_slug(apps, schema_editor):
|
||||
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
|
||||
for id, slug in VIRTUALMACHINE_STATUS_CHOICES:
|
||||
VirtualMachine.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0010_cluster_add_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# VirtualMachine.status
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=virtualmachine_status_to_slug
|
||||
),
|
||||
|
||||
]
|
23
netbox/virtualization/migrations/0012_vm_name_nonunique.py
Normal file
23
netbox/virtualization/migrations/0012_vm_name_nonunique.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-09 16:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
('virtualization', '0011_3569_virtualmachine_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='virtualmachine',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='virtualmachine',
|
||||
unique_together={('cluster', 'tenant', 'name')},
|
||||
),
|
||||
]
|
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-15 18:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0012_vm_name_nonunique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='virtualmachine',
|
||||
options={'ordering': ('name', 'pk')},
|
||||
),
|
||||
]
|
@@ -8,7 +8,15 @@ from taggit.managers import TaggableManager
|
||||
from dcim.models import Device
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from .constants import *
|
||||
from .choices import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Cluster',
|
||||
'ClusterGroup',
|
||||
'ClusterType',
|
||||
'VirtualMachine',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -103,6 +111,13 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='clusters',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -122,6 +137,9 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['name', 'type', 'group', 'site', 'comments']
|
||||
clone_fields = [
|
||||
'type', 'group', 'tenant', 'site',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -150,6 +168,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
self.type.name,
|
||||
self.group.name if self.group else None,
|
||||
self.site.name if self.site else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@@ -182,12 +201,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64,
|
||||
unique=True
|
||||
max_length=64
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=VM_STATUS_CHOICES,
|
||||
default=DEVICE_STATUS_ACTIVE,
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
choices=VirtualMachineStatusChoices,
|
||||
default=VirtualMachineStatusChoices.STATUS_ACTIVE,
|
||||
verbose_name='Status'
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
@@ -243,9 +262,21 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
csv_headers = [
|
||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
||||
]
|
||||
|
||||
STATUS_CLASS_MAP = {
|
||||
'active': 'success',
|
||||
'offline': 'warning',
|
||||
'staged': 'primary',
|
||||
}
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ('name', 'pk') # Name may be non-unique
|
||||
unique_together = [
|
||||
['cluster', 'tenant', 'name']
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -253,6 +284,20 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
|
||||
# Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary
|
||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||
# of the uniqueness constraint without manual intervention.
|
||||
if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter(
|
||||
name=self.name, tenant__isnull=True
|
||||
):
|
||||
raise ValidationError({
|
||||
'name': 'A virtual machine with this name already exists.'
|
||||
})
|
||||
|
||||
super().validate_unique(exclude)
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
@@ -286,7 +331,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return VM_STATUS_CLASSES[self.status]
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
@property
|
||||
def primary_ip(self):
|
||||
|
@@ -84,13 +84,14 @@ class ClusterGroupTable(BaseTable):
|
||||
class ClusterTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices')
|
||||
vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Cluster
|
||||
fields = ('pk', 'name', 'type', 'group', 'site', 'device_count', 'vm_count')
|
||||
fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
||||
|
||||
|
||||
#
|
||||
|
@@ -2,13 +2,37 @@ from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_TAGGED
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.models import Interface
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from utilities.testing import APITestCase
|
||||
from utilities.testing import APITestCase, choices_to_dict
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
|
||||
def test_root(self):
|
||||
|
||||
url = reverse('virtualization-api:api-root')
|
||||
response = self.client.get('{}?format=api'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_choices(self):
|
||||
|
||||
url = reverse('virtualization-api:field-choice-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# VirtualMachine
|
||||
self.assertEqual(choices_to_dict(response.data.get('virtual-machine:status')), VirtualMachineStatusChoices.as_dict())
|
||||
|
||||
# Interface
|
||||
self.assertEqual(choices_to_dict(response.data.get('interface:type')), VMInterfaceTypeChoices.as_dict())
|
||||
|
||||
|
||||
class ClusterTypeTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -489,17 +513,17 @@ class InterfaceTest(APITestCase):
|
||||
self.interface1 = Interface.objects.create(
|
||||
virtual_machine=self.virtualmachine,
|
||||
name='Test Interface 1',
|
||||
type=IFACE_TYPE_VIRTUAL
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
)
|
||||
self.interface2 = Interface.objects.create(
|
||||
virtual_machine=self.virtualmachine,
|
||||
name='Test Interface 2',
|
||||
type=IFACE_TYPE_VIRTUAL
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
)
|
||||
self.interface3 = Interface.objects.create(
|
||||
virtual_machine=self.virtualmachine,
|
||||
name='Test Interface 3',
|
||||
type=IFACE_TYPE_VIRTUAL
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
)
|
||||
|
||||
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
|
||||
@@ -551,7 +575,7 @@ class InterfaceTest(APITestCase):
|
||||
data = {
|
||||
'virtual_machine': self.virtualmachine.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan3.id,
|
||||
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
||||
}
|
||||
@@ -598,21 +622,21 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'virtual_machine': self.virtualmachine.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
{
|
||||
'virtual_machine': self.virtualmachine.pk,
|
||||
'name': 'Test Interface 5',
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
{
|
||||
'virtual_machine': self.virtualmachine.pk,
|
||||
'name': 'Test Interface 6',
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
|
@@ -1,14 +1,14 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from virtualization.constants import *
|
||||
from virtualization.choices import *
|
||||
from virtualization.filters import *
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class ClusterTypeTestCase(TestCase):
|
||||
queryset = ClusterType.objects.all()
|
||||
filterset = ClusterTypeFilter
|
||||
filterset = ClusterTypeFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -36,7 +36,7 @@ class ClusterTypeTestCase(TestCase):
|
||||
|
||||
class ClusterGroupTestCase(TestCase):
|
||||
queryset = ClusterGroup.objects.all()
|
||||
filterset = ClusterGroupFilter
|
||||
filterset = ClusterGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -64,7 +64,7 @@ class ClusterGroupTestCase(TestCase):
|
||||
|
||||
class ClusterTestCase(TestCase):
|
||||
queryset = Cluster.objects.all()
|
||||
filterset = ClusterFilter
|
||||
filterset = ClusterFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -146,7 +146,7 @@ class ClusterTestCase(TestCase):
|
||||
|
||||
class VirtualMachineTestCase(TestCase):
|
||||
queryset = VirtualMachine.objects.all()
|
||||
filterset = VirtualMachineFilter
|
||||
filterset = VirtualMachineFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -203,9 +203,9 @@ class VirtualMachineTestCase(TestCase):
|
||||
DeviceRole.objects.bulk_create(roles)
|
||||
|
||||
vms = (
|
||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], status=DEVICE_STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], status=DEVICE_STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], status=DEVICE_STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(vms)
|
||||
|
||||
@@ -243,7 +243,7 @@ class VirtualMachineTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [DEVICE_STATUS_ACTIVE, DEVICE_STATUS_STAGED]}
|
||||
params = {'status': [VirtualMachineStatusChoices.STATUS_ACTIVE, VirtualMachineStatusChoices.STATUS_STAGED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cluster_group(self):
|
||||
@@ -309,7 +309,7 @@ class VirtualMachineTestCase(TestCase):
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = InterfaceFilter
|
||||
filterset = InterfaceFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
45
netbox/virtualization/tests/test_models.py
Normal file
45
netbox/virtualization/tests/test_models.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
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):
|
||||
|
||||
vm1 = VirtualMachine(
|
||||
cluster=self.cluster,
|
||||
name='Test VM 1'
|
||||
)
|
||||
vm1.save()
|
||||
|
||||
vm2 = VirtualMachine(
|
||||
cluster=vm1.cluster,
|
||||
name=vm1.name
|
||||
)
|
||||
|
||||
# Two VMs assigned to the same Cluster and no Tenant should fail validation
|
||||
with self.assertRaises(ValidationError):
|
||||
vm2.full_clean()
|
||||
|
||||
tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
||||
vm1.tenant = tenant
|
||||
vm1.save()
|
||||
vm2.tenant = tenant
|
||||
|
||||
# Two VMs assigned to the same Cluster and the same Tenant should fail validation
|
||||
with self.assertRaises(ValidationError):
|
||||
vm2.full_clean()
|
||||
|
||||
vm2.tenant = None
|
||||
|
||||
# Two VMs assigned to the same Cluster and different Tenants should pass validation
|
||||
vm2.full_clean()
|
||||
vm2.save()
|
@@ -10,7 +10,12 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
|
||||
class ClusterGroupTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['virtualization.view_clustergroup'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'virtualization.view_clustergroup',
|
||||
'virtualization.add_clustergroup',
|
||||
]
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -27,11 +32,30 @@ class ClusterGroupTestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_clustergroup_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Cluster Group 4,cluster-group-4",
|
||||
"Cluster Group 5,cluster-group-5",
|
||||
"Cluster Group 6,cluster-group-6",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('virtualization:clustergroup_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(ClusterGroup.objects.count(), 6)
|
||||
|
||||
|
||||
class ClusterTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['virtualization.view_clustertype'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'virtualization.view_clustertype',
|
||||
'virtualization.add_clustertype',
|
||||
]
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -48,11 +72,30 @@ class ClusterTypeTestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_clustertype_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Cluster Type 4,cluster-type-4",
|
||||
"Cluster Type 5,cluster-type-5",
|
||||
"Cluster Type 6,cluster-type-6",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('virtualization:clustertype_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(ClusterType.objects.count(), 6)
|
||||
|
||||
|
||||
class ClusterTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['virtualization.view_cluster'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'virtualization.view_cluster',
|
||||
'virtualization.add_cluster',
|
||||
]
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -85,11 +128,30 @@ class ClusterTestCase(TestCase):
|
||||
response = self.client.get(cluster.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_cluster_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,type",
|
||||
"Cluster 4,Cluster Type 1",
|
||||
"Cluster 5,Cluster Type 1",
|
||||
"Cluster 6,Cluster Type 1",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('virtualization:cluster_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(Cluster.objects.count(), 6)
|
||||
|
||||
|
||||
class VirtualMachineTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['virtualization.view_virtualmachine'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'virtualization.view_virtualmachine',
|
||||
'virtualization.add_virtualmachine',
|
||||
]
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -120,3 +182,17 @@ class VirtualMachineTestCase(TestCase):
|
||||
virtualmachine = VirtualMachine.objects.first()
|
||||
response = self.client.get(virtualmachine.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_virtualmachine_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,cluster",
|
||||
"Virtual Machine 4,Cluster 1",
|
||||
"Virtual Machine 5,Cluster 1",
|
||||
"Virtual Machine 6,Cluster 1",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('virtualization:virtualmachine_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(VirtualMachine.objects.count(), 6)
|
||||
|
@@ -96,10 +96,10 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class ClusterListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'virtualization.view_cluster'
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site', 'tenant')
|
||||
table = tables.ClusterTable
|
||||
filter = filters.ClusterFilter
|
||||
filter_form = forms.ClusterFilterForm
|
||||
filterset = filters.ClusterFilterSet
|
||||
filterset_form = forms.ClusterFilterForm
|
||||
template_name = 'virtualization/cluster_list.html'
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ class ClusterBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'virtualization.change_cluster'
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
||||
filter = filters.ClusterFilter
|
||||
filterset = filters.ClusterFilterSet
|
||||
table = tables.ClusterTable
|
||||
form = forms.ClusterBulkEditForm
|
||||
default_return_url = 'virtualization:cluster_list'
|
||||
@@ -158,7 +158,7 @@ class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'virtualization.delete_cluster'
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
||||
filter = filters.ClusterFilter
|
||||
filterset = filters.ClusterFilterSet
|
||||
table = tables.ClusterTable
|
||||
default_return_url = 'virtualization:cluster_list'
|
||||
|
||||
@@ -254,8 +254,8 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
|
||||
class VirtualMachineListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'virtualization.view_virtualmachine'
|
||||
queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role', 'primary_ip4', 'primary_ip6')
|
||||
filter = filters.VirtualMachineFilter
|
||||
filter_form = forms.VirtualMachineFilterForm
|
||||
filterset = filters.VirtualMachineFilterSet
|
||||
filterset_form = forms.VirtualMachineFilterForm
|
||||
table = tables.VirtualMachineDetailTable
|
||||
template_name = 'virtualization/virtualmachine_list.html'
|
||||
|
||||
@@ -310,7 +310,7 @@ class VirtualMachineBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'virtualization.change_virtualmachine'
|
||||
queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
|
||||
filter = filters.VirtualMachineFilter
|
||||
filterset = filters.VirtualMachineFilterSet
|
||||
table = tables.VirtualMachineTable
|
||||
form = forms.VirtualMachineBulkEditForm
|
||||
default_return_url = 'virtualization:virtualmachine_list'
|
||||
@@ -319,7 +319,7 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'virtualization.delete_virtualmachine'
|
||||
queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
|
||||
filter = filters.VirtualMachineFilter
|
||||
filterset = filters.VirtualMachineFilterSet
|
||||
table = tables.VirtualMachineTable
|
||||
default_return_url = 'virtualization:virtualmachine_list'
|
||||
|
||||
@@ -376,6 +376,6 @@ class VirtualMachineBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentC
|
||||
form = forms.VirtualMachineBulkAddInterfaceForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
filter = filters.VirtualMachineFilter
|
||||
filterset = filters.VirtualMachineFilterSet
|
||||
table = tables.VirtualMachineTable
|
||||
default_return_url = 'virtualization:virtualmachine_list'
|
||||
|
Reference in New Issue
Block a user