Merge branch 'develop-2.2' into develop-2.1

This commit is contained in:
Jeremy Stretch
2017-09-29 12:30:36 -04:00
committed by GitHub
105 changed files with 4662 additions and 562 deletions

View File

@@ -16,6 +16,7 @@ from dcim.models import (
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster
#
@@ -403,7 +404,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceRole
fields = ['id', 'name', 'slug', 'color']
fields = ['id', 'name', 'slug', 'color', 'vm_role']
class NestedDeviceRoleSerializer(serializers.ModelSerializer):
@@ -446,6 +447,15 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'family', 'address']
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta:
model = Cluster
fields = ['id', 'url', 'name']
class DeviceSerializer(CustomFieldModelSerializer):
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
@@ -459,13 +469,14 @@ class DeviceSerializer(CustomFieldModelSerializer):
primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer()
class Meta:
model = Device
fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'comments', 'custom_fields',
'cluster', 'comments', 'custom_fields',
]
def get_parent_device(self, obj):
@@ -485,7 +496,7 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
model = Device
fields = [
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'comments', 'custom_fields',
]
validators = []

View File

@@ -93,13 +93,15 @@ IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
VIFACE_FF_CHOICES = [
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
VIFACE_FF_CHOICES,
],
[
'Ethernet (fixed)',

View File

@@ -10,6 +10,7 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
from virtualization.models import Cluster
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
@@ -408,6 +409,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(),
label='Rack (ID)',
)
cluster_id = NullableModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
)
model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug',
queryset=DeviceType.objects.all(),

View File

@@ -12,10 +12,10 @@ from ipam.models import IPAddress
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, ArrayFieldSelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField,
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
SlugField, FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
from .models import (
@@ -43,15 +43,6 @@ def get_device_by_name_or_pk(name):
return device
class DeviceComponentForm(BootstrapMixin, forms.Form):
"""
Allow inclusion of the parent device as context for limiting field choices.
"""
def __init__(self, device, *args, **kwargs):
self.device = device
super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
@@ -532,7 +523,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class ConsolePortTemplateCreateForm(DeviceComponentForm):
class ConsolePortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -546,7 +537,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
class ConsoleServerPortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -560,7 +551,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class PowerPortTemplateCreateForm(DeviceComponentForm):
class PowerPortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -574,7 +565,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
}
class PowerOutletTemplateCreateForm(DeviceComponentForm):
class PowerOutletTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -588,7 +579,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
}
class InterfaceTemplateCreateForm(DeviceComponentForm):
class InterfaceTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
@@ -613,7 +604,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
}
class DeviceBayTemplateCreateForm(DeviceComponentForm):
class DeviceBayTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -626,7 +617,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color']
fields = ['name', 'slug', 'color', 'vm_role']
#
@@ -1007,11 +998,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
name_pattern = ExpandableNameField(label='Name')
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
class Meta:
model = Interface
fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False, initial=True)
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False)
#
@@ -1028,7 +1020,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
}
class ConsolePortCreateForm(DeviceComponentForm):
class ConsolePortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1197,7 +1189,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
}
class ConsoleServerPortCreateForm(DeviceComponentForm):
class ConsoleServerPortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1289,7 +1281,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
}
class PowerPortCreateForm(DeviceComponentForm):
class PowerPortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1458,7 +1450,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
}
class PowerOutletCreateForm(DeviceComponentForm):
class PowerOutletCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1563,7 +1555,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
)
class InterfaceCreateForm(DeviceComponentForm):
class InterfaceCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False)
@@ -1582,9 +1574,9 @@ class InterfaceCreateForm(DeviceComponentForm):
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device
if self.device is not None:
if self.parent is not None:
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
device=self.device, form_factor=IFACE_FF_LAG
device=self.parent, form_factor=IFACE_FF_LAG
)
else:
self.fields['lag'].queryset = Interface.objects.none()
@@ -1810,7 +1802,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
}
class DeviceBayCreateForm(DeviceComponentForm):
class DeviceBayCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 14:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('dcim', '0043_device_component_name_lengths'),
]
operations = [
migrations.AddField(
model_name='device',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
),
migrations.AddField(
model_name='interface',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='interface',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-29 16:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0044_virtualization'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='vm_role',
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
),
]

View File

@@ -795,11 +795,17 @@ class DeviceBayTemplate(models.Model):
class DeviceRole(models.Model):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
color to be used when displaying rack elevations.
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
virtual machines as well.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = ColorField()
vm_role = models.BooleanField(
default=True,
verbose_name="VM Role",
help_text="Virtual machines may be assigned to this role"
)
class Meta:
ordering = ['name']
@@ -880,6 +886,13 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='Primary IPv6'
)
cluster = models.ForeignKey(
to='virtualization.Cluster',
on_delete=models.SET_NULL,
related_name='devices',
blank=True,
null=True
)
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
@@ -986,6 +999,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
})
def save(self, *args, **kwargs):
is_new = not bool(self.pk)
@@ -1229,13 +1248,26 @@ class PowerOutlet(models.Model):
@python_2_unicode_compatible
class Interface(models.Model):
"""
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
of an InterfaceConnection.
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
Interface via the creation of an InterfaceConnection.
"""
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
device = models.ForeignKey(
to='Device',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
lag = models.ForeignKey(
'self',
models.SET_NULL,
to='self',
on_delete=models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
@@ -1264,6 +1296,18 @@ class Interface(models.Model):
def clean(self):
# An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("An interface must belong to either a device or a virtual machine.")
# VM interfaces must be virtual
if self.virtual_machine and self.form_factor not in VIRTUAL_IFACE_TYPES:
raise ValidationError({
'form_factor': "Virtual machines cannot have physical interfaces."
})
# Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({
@@ -1293,6 +1337,10 @@ class Interface(models.Model):
)
})
@property
def parent(self):
return self.device or self.virtual_machine
@property
def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES

View File

@@ -362,14 +362,14 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = DeviceRole
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
fields = ('pk', 'name', 'device_count', 'color', 'vm_role', 'slug', 'actions')
#

View File

@@ -130,7 +130,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports

View File

@@ -23,7 +23,8 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_S
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
@@ -60,87 +61,6 @@ def expand_pattern(string):
yield "{0}{1}".format(lead, i)
class ComponentCreateView(View):
parent_model = None
parent_field = None
model = None
form = None
model_form = None
def get(self, request, pk):
parent = get_object_or_404(self.parent_model, pk=pk)
form = self.form(parent, initial=request.GET)
return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})
def post(self, request, pk):
parent = get_object_or_404(self.parent_model, pk=pk)
form = self.form(parent, request.POST)
if form.is_valid():
new_components = []
data = deepcopy(form.cleaned_data)
for name in form.cleaned_data['name_pattern']:
component_data = {
self.parent_field: parent.pk,
'name': name,
}
# Replace objects with their primary key to keep component_form.clean() happy
for k, v in data.items():
if hasattr(v, 'pk'):
component_data[k] = v.pk
else:
component_data[k] = v
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
# Assign errors on the child form's name field to name_pattern on the parent form
if field == 'name':
field = 'name_pattern'
for e in errors:
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
if not form.errors:
self.model.objects.bulk_create(new_components)
messages.success(request, "Added {} {} to {}.".format(
len(new_components), self.model._meta.verbose_name_plural, parent
))
if '_addanother' in request.POST:
return redirect(request.path)
else:
return redirect(parent.get_absolute_url())
return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})
class ComponentEditView(ObjectEditView):
def get_return_url(self, request, obj):
return obj.device.get_absolute_url()
class ComponentDeleteView(ObjectDeleteView):
def get_return_url(self, request, obj):
return obj.device.get_absolute_url()
class BulkDisconnectView(View):
"""
An extendable view for disconnection console/power/interface components in bulk.
@@ -192,7 +112,7 @@ class RegionListView(ObjectListView):
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_region'
model = Region
form_class = forms.RegionForm
model_form = forms.RegionForm
def get_return_url(self, request, obj):
return reverse('dcim:region_list')
@@ -257,7 +177,7 @@ class SiteView(View):
class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_site'
model = Site
form_class = forms.SiteForm
model_form = forms.SiteForm
template_name = 'dcim/site_edit.html'
default_return_url = 'dcim:site_list'
@@ -304,7 +224,7 @@ class RackGroupListView(ObjectListView):
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
model_form = forms.RackGroupForm
def get_return_url(self, request, obj):
return reverse('dcim:rackgroup_list')
@@ -343,7 +263,7 @@ class RackRoleListView(ObjectListView):
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackrole'
model = RackRole
form_class = forms.RackRoleForm
model_form = forms.RackRoleForm
def get_return_url(self, request, obj):
return reverse('dcim:rackrole_list')
@@ -446,7 +366,7 @@ class RackView(View):
class RackCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rack'
model = Rack
form_class = forms.RackForm
model_form = forms.RackForm
template_name = 'dcim/rack_edit.html'
default_return_url = 'dcim:rack_list'
@@ -502,7 +422,7 @@ class RackReservationListView(ObjectListView):
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackreservation'
model = RackReservation
form_class = forms.RackReservationForm
model_form = forms.RackReservationForm
def alter_obj(self, obj, request, args, kwargs):
if not obj.pk:
@@ -546,7 +466,7 @@ class ManufacturerListView(ObjectListView):
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_manufacturer'
model = Manufacturer
form_class = forms.ManufacturerForm
model_form = forms.ManufacturerForm
def get_return_url(self, request, obj):
return reverse('dcim:manufacturer_list')
@@ -638,7 +558,7 @@ class DeviceTypeView(View):
class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicetype'
model = DeviceType
form_class = forms.DeviceTypeForm
model_form = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html'
default_return_url = 'dcim:devicetype_list'
@@ -690,6 +610,7 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = ConsolePortTemplate
form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -708,6 +629,7 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -724,6 +646,7 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerPortTemplate
form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -740,6 +663,7 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = PowerOutletTemplate
form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -756,6 +680,7 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = InterfaceTemplate
form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm
template_name = 'dcim/device_component_add.html'
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
@@ -780,6 +705,7 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/device_component_add.html'
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -802,7 +728,7 @@ class DeviceRoleListView(ObjectListView):
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicerole'
model = DeviceRole
form_class = forms.DeviceRoleForm
model_form = forms.DeviceRoleForm
def get_return_url(self, request, obj):
return reverse('dcim:devicerole_list')
@@ -833,7 +759,7 @@ class PlatformListView(ObjectListView):
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_platform'
model = Platform
form_class = forms.PlatformForm
model_form = forms.PlatformForm
def get_return_url(self, request, obj):
return reverse('dcim:platform_list')
@@ -991,7 +917,7 @@ class DeviceConfigView(PermissionRequiredMixin, View):
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device
form_class = forms.DeviceForm
model_form = forms.DeviceForm
template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list'
@@ -1063,6 +989,7 @@ class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = ConsolePort
form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_consoleport')
@@ -1144,12 +1071,14 @@ def consoleport_disconnect(request, pk):
class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleport'
model = ConsolePort
form_class = forms.ConsolePortForm
parent_field = 'device'
model_form = forms.ConsolePortForm
class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleport'
model = ConsolePort
parent_field = 'device'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -1177,6 +1106,7 @@ class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = ConsoleServerPort
form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_consoleserverport')
@@ -1261,12 +1191,14 @@ def consoleserverport_disconnect(request, pk):
class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
form_class = forms.ConsoleServerPortForm
parent_field = 'device'
model_form = forms.ConsoleServerPortForm
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleserverport'
model = ConsoleServerPort
parent_field = 'device'
class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1296,6 +1228,7 @@ class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerPort
form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_powerport')
@@ -1377,12 +1310,14 @@ def powerport_disconnect(request, pk):
class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_powerport'
model = PowerPort
form_class = forms.PowerPortForm
parent_field = 'device'
model_form = forms.PowerPortForm
class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_powerport'
model = PowerPort
parent_field = 'device'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -1410,6 +1345,7 @@ class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerOutlet
form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_poweroutlet')
@@ -1494,12 +1430,14 @@ def poweroutlet_disconnect(request, pk):
class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
form_class = forms.PowerOutletForm
parent_field = 'device'
model_form = forms.PowerOutletForm
class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_poweroutlet'
model = PowerOutlet
parent_field = 'device'
class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1531,17 +1469,20 @@ class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
model = Interface
form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm
template_name = 'dcim/device_component_add.html'
class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_interface'
model = Interface
form_class = forms.InterfaceForm
parent_field = 'device'
model_form = forms.InterfaceForm
class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_interface'
model = Interface
parent_field = 'device'
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1582,17 +1523,20 @@ class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
model = DeviceBay
form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm
template_name = 'dcim/device_component_add.html'
class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_devicebay'
model = DeviceBay
form_class = forms.DeviceBayForm
parent_field = 'device'
model_form = forms.DeviceBayForm
class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_devicebay'
model = DeviceBay
parent_field = 'device'
@permission_required('dcim.change_devicebay')
@@ -1653,109 +1597,67 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
# Bulk device component creation
# Bulk Device component creation
#
class DeviceBulkAddComponentView(View):
"""
Add one or more components (e.g. interfaces) to a selected set of Devices.
"""
form = forms.DeviceBulkAddComponentForm
model = None
model_form = None
def get(self):
return redirect('dcim:device_list')
def post(self, request):
# Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'):
pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
if '_create' in request.POST:
form = self.form(request.POST)
if form.is_valid():
new_components = []
data = deepcopy(form.cleaned_data)
for device in data['pk']:
names = data['name_pattern']
for name in names:
component_data = {
'device': device.pk,
'name': name,
}
component_data.update(data)
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
for e in errors:
form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e)))
if not form.errors:
self.model.objects.bulk_create(new_components)
messages.success(request, "Added {} {} to {} devices.".format(
len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
))
return redirect('dcim:device_list')
else:
form = self.form(initial={'pk': pk_list})
selected_devices = Device.objects.filter(pk__in=pk_list)
if not selected_devices:
messages.warning(request, "No devices were selected.")
return redirect('dcim:device_list')
return render(request, 'dcim/device_bulk_add_component.html', {
'form': form,
'component_name': self.model._meta.verbose_name_plural,
'selected_devices': selected_devices,
'return_url': reverse('dcim:device_list'),
})
class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = ConsolePort
model_form = forms.ConsolePortForm
table = tables.DeviceTable
class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = ConsoleServerPort
model_form = forms.ConsoleServerPortForm
table = tables.DeviceTable
class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = PowerPort
model_form = forms.PowerPortForm
table = tables.DeviceTable
class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = PowerOutlet
model_form = forms.PowerOutletForm
table = tables.DeviceTable
class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddInterfaceForm
model = Interface
model_form = forms.InterfaceForm
table = tables.DeviceTable
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = DeviceBay
model_form = forms.DeviceBayForm
table = tables.DeviceTable
#
@@ -1899,7 +1801,8 @@ class InterfaceConnectionsListView(ObjectListView):
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_inventoryitem'
model = InventoryItem
form_class = forms.InventoryItemForm
parent_field = 'device'
model_form = forms.InventoryItemForm
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs:
@@ -1910,3 +1813,4 @@ class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_inventoryitem'
model = InventoryItem
parent_field = 'device'