1
0
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:
Saria Hajjar
2020-01-16 15:33:42 +00:00
1030 changed files with 94332 additions and 6656 deletions

View File

@@ -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(),

View File

@@ -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']

View 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'),
)

View File

@@ -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',
}

View File

@@ -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',

View File

@@ -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"
}
}
]

View File

@@ -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(

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View 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'),
),
]

View File

@@ -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,
),
]

View File

@@ -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
),
]

View 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')},
),
]

View File

@@ -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')},
),
]

View File

@@ -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):

View File

@@ -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')
#

View File

@@ -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],
},

View File

@@ -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):

View 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()

View File

@@ -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)

View File

@@ -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'