Collapsed VCMembership into the Device model (WIP)

This commit is contained in:
Jeremy Stretch 2018-01-31 22:47:27 -05:00
parent 6b101d2c49
commit a4019be28c
15 changed files with 485 additions and 765 deletions

View File

@ -14,7 +14,7 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis, VCMembership RackReservation, RackRole, Region, Site, VirtualChassis,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress, VLAN from ipam.models import IPAddress, VLAN
@ -489,7 +489,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
primary_ip4 = DeviceIPAddressSerializer() primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer() primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField() parent_device = serializers.SerializerMethodField()
virtual_chassis = serializers.SerializerMethodField()
cluster = NestedClusterSerializer() cluster = NestedClusterSerializer()
class Meta: class Meta:
@ -497,7 +496,8 @@ class DeviceSerializer(CustomFieldModelSerializer):
fields = [ fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'virtual_chassis', 'status', 'primary_ip', 'site', 'rack', 'position', 'face', 'parent_device', 'virtual_chassis', 'status', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'comments', 'custom_fields', 'created', 'last_updated', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'comments', 'custom_fields', 'created',
'last_updated',
] ]
def get_parent_device(self, obj): def get_parent_device(self, obj):
@ -510,16 +510,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
return data return data
def get_virtual_chassis(self, obj):
try:
vc_membership = obj.vc_membership
except VCMembership.DoesNotExist:
return None
context = {'request': self.context['request']}
data = NestedVirtualChassisSerializer(instance=vc_membership.virtual_chassis, context=context).data
data['vc_membership'] = NestedVCMembershipSerializer(instance=vc_membership, context=context).data
return data
class WritableDeviceSerializer(CustomFieldModelSerializer): class WritableDeviceSerializer(CustomFieldModelSerializer):
@ -833,10 +823,11 @@ class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
# #
class VirtualChassisSerializer(serializers.ModelSerializer): class VirtualChassisSerializer(serializers.ModelSerializer):
master = NestedDeviceSerializer()
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'domain'] fields = ['id', 'master', 'domain']
class NestedVirtualChassisSerializer(serializers.ModelSerializer): class NestedVirtualChassisSerializer(serializers.ModelSerializer):
@ -851,44 +842,4 @@ class WritableVirtualChassisSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'domain'] fields = ['id', 'master', 'domain']
#
# Virtual chassis memberships
#
class VCMembershipSerializer(serializers.ModelSerializer):
virtual_chassis = NestedVirtualChassisSerializer()
device = NestedDeviceSerializer()
class Meta:
model = VCMembership
fields = ['id', 'virtual_chassis', 'device', 'position', 'is_master', 'priority']
class NestedVCMembershipSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:vcmembership-detail')
class Meta:
model = VCMembership
fields = ['id', 'url', 'position', 'is_master', 'priority']
class WritableVCMembershipSerializer(ValidatedModelSerializer):
class Meta:
model = VCMembership
fields = ['id', 'virtual_chassis', 'device', 'position', 'is_master', 'priority']
def validate(self, data):
# Validate uniqueness of (virtual_chassis, position)
validator = UniqueTogetherValidator(queryset=VCMembership.objects.all(), fields=('virtual_chassis', 'position'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableVCMembershipSerializer, self).validate(data)
return data

View File

@ -62,7 +62,6 @@ router.register(r'interface-connections', views.InterfaceConnectionViewSet)
# Virtual chassis # Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet) router.register(r'virtual-chassis', views.VirtualChassisViewSet)
router.register(r'vc-memberships', views.VCMembershipViewSet)
# Miscellaneous # Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device') router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')

View File

@ -16,7 +16,7 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VCMembership, VirtualChassis RackReservation, RackRole, Region, Site, VirtualChassis,
) )
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
@ -403,32 +403,6 @@ class VirtualChassisViewSet(ModelViewSet):
write_serializer_class = serializers.WritableVirtualChassisSerializer write_serializer_class = serializers.WritableVirtualChassisSerializer
class VCMembershipViewSet(ModelViewSet):
queryset = VCMembership.objects.select_related('virtual_chassis', 'device')
serializer_class = serializers.VCMembershipSerializer
write_serializer_class = serializers.WritableVCMembershipSerializer
filter_class = filters.VCMembershipFilter
def create(self, request, *args, **kwargs):
with transaction.atomic():
# Automatically create a new VirtualChassis for new VCMemberships with no VC specified
if isinstance(request.data, list):
for i, vcm in enumerate(request.data):
if not vcm.get('virtual_chassis') and vcm.get('is_master'):
vc = VirtualChassis()
vc.save()
request.data[i]['virtual_chassis'] = vc.pk
else:
if not request.data.get('virtual_chassis') and request.data.get('is_master'):
vc = VirtualChassis()
vc.save()
request.data['virtual_chassis'] = vc.pk
return super(VCMembershipViewSet, self).create(request, *args, **kwargs)
# #
# Miscellaneous # Miscellaneous
# #

View File

@ -6,6 +6,3 @@ from django.apps import AppConfig
class DCIMConfig(AppConfig): class DCIMConfig(AppConfig):
name = "dcim" name = "dcim"
verbose_name = "DCIM" verbose_name = "DCIM"
def ready(self):
import dcim.signals

View File

@ -17,7 +17,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis, VCMembership, RackReservation, RackRole, Region, Site, VirtualChassis,
) )
@ -680,13 +680,6 @@ class VirtualChassisFilter(django_filters.FilterSet):
fields = ['domain'] fields = ['domain']
class VCMembershipFilter(django_filters.FilterSet):
class Meta:
model = VCMembership
fields = ['virtual_chassis', 'device', 'position', 'is_master', 'priority']
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',

View File

@ -32,7 +32,7 @@ from .models import (
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
RackRole, Region, Site, VCMembership, VirtualChassis RackRole, Region, Site, VirtualChassis
) )
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = '{\d+\}'
@ -2265,94 +2265,49 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
master = forms.ModelChoiceField(queryset=Device.objects.all())
class Meta:
model = VirtualChassis
fields = ['domain']
def __init__(self, *args, **kwargs):
super(VirtualChassisForm, self).__init__(*args, **kwargs)
if self.instance:
vc_memberships = self.instance.memberships.all()
self.fields['master'].queryset = Device.objects.filter(pk__in=[vcm.device_id for vcm in vc_memberships])
self.initial['master'] = self.instance.master
def save(self, commit=True):
instance = super(VirtualChassisForm, self).save(commit=commit)
# Update the master membership if it has been changed
master = self.cleaned_data['master']
if instance.pk and instance.master != master:
VCMembership.objects.filter(virtual_chassis=self.instance).update(is_master=False)
VCMembership.objects.filter(virtual_chassis=self.instance, device=master).update(is_master=True)
return instance
class DeviceSelectionForm(forms.Form): class DeviceSelectionForm(forms.Form):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
master = forms.ModelChoiceField(queryset=Device.objects.all())
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['master', 'domain'] fields = ['master', 'domain']
def __init__(self, candidate_pks, *args, **kwargs):
super(VirtualChassisCreateForm, self).__init__(*args, **kwargs)
self.fields['master'].queryset = Device.objects.filter(pk__in=candidate_pks)
# class VCAddMemberForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
# # site = forms.ModelChoiceField(
# VC memberships # queryset=Site.objects.all(),
# # label='Site',
# required=False,
class VCMembershipForm(BootstrapMixin, forms.ModelForm): # widget=forms.Select(
# attrs={'filter-for': 'rack'}
class Meta: # )
model = VCMembership # )
fields = ['position', 'priority'] # rack = ChainedModelChoiceField(
# queryset=Rack.objects.all(),
# chains=(
class VCMembershipCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): # ('site', 'site'),
site = forms.ModelChoiceField( # ),
queryset=Site.objects.all(), # label='Rack',
label='Site', # required=False,
required=False, # widget=APISelect(
widget=forms.Select( # api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'rack'} # attrs={'filter-for': 'device', 'nullable': 'true'}
) # )
) # )
rack = ChainedModelChoiceField( # device = ChainedModelChoiceField(
queryset=Rack.objects.all(), # queryset=Device.objects.all(),
chains=( # chains=(
('site', 'site'), # ('site', 'site'),
), # ('rack', 'rack'),
label='Rack', # ),
required=False, # label='Device',
widget=APISelect( # widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}', # api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
attrs={'filter-for': 'device', 'nullable': 'true'} # display_field='display_name'
) # )
) # )
device = ChainedModelChoiceField( # vc_position = forms.IntegerField(label='Position')
queryset=Device.objects.all(), # vc_priority = forms.IntegerField(required=False, label='Priority')
chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name'
)
)
class Meta:
model = VCMembership
fields = ['site', 'rack', 'device', 'position', 'priority']

View File

@ -14,34 +14,31 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='VCMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveSmallIntegerField(validators=[django.core.validators.MaxValueValidator(255)])),
('is_master', models.BooleanField(default=False)),
('priority', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)])),
('device', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vc_membership', to='dcim.Device')),
],
options={
'verbose_name': 'VC membership',
'ordering': ['virtual_chassis', 'position'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='VirtualChassis', name='VirtualChassis',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(blank=True, max_length=30)), ('domain', models.CharField(blank=True, max_length=30)),
('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='vcmembership', model_name='device',
name='virtual_chassis', name='virtual_chassis',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='dcim.VirtualChassis'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
),
migrations.AddField(
model_name='device',
name='vc_position',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AddField(
model_name='device',
name='vc_priority',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='vcmembership', name='device',
unique_together=set([('virtual_chassis', 'position')]), unique_together=set([('virtual_chassis', 'vc_position'), ('rack', 'position', 'face')]),
), ),
] ]

View File

@ -867,6 +867,23 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
virtual_chassis = models.ForeignKey(
to='VirtualChassis',
on_delete=models.SET_NULL,
related_name='members',
blank=True,
null=True
)
vc_position = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[MaxValueValidator(255)]
)
vc_priority = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[MaxValueValidator(255)]
)
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment) images = GenericRelation(ImageAttachment)
@ -880,7 +897,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ['rack', 'position', 'face'] unique_together = [
['rack', 'position', 'face'],
['virtual_chassis', 'vc_position'],
]
permissions = ( permissions = (
('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_read', 'Read-only access to devices via NAPALM'),
('napalm_write', 'Read/write access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'),
@ -1079,13 +1099,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
else: else:
return None return None
@property
def virtual_chassis(self):
try:
return VCMembership.objects.get(device=self).virtual_chassis
except VCMembership.DoesNotExist:
return None
@property @property
def vc_interfaces(self): def vc_interfaces(self):
""" """
@ -1593,70 +1606,18 @@ class VirtualChassis(models.Model):
""" """
A collection of Devices which operate with a shared control plane (e.g. a switch stack). A collection of Devices which operate with a shared control plane (e.g. a switch stack).
""" """
master = models.OneToOneField(
to='Device',
on_delete=models.PROTECT,
related_name='vc_master_for'
)
domain = models.CharField( domain = models.CharField(
max_length=30, max_length=30,
blank=True blank=True
) )
def __str__(self): def __str__(self):
return self.master.name return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
def get_absolute_url(self): def get_absolute_url(self):
return self.master.get_absolute_url() return self.master.get_absolute_url()
@property
def master(self):
master_vcm = VCMembership.objects.filter(virtual_chassis=self, is_master=True).first()
return master_vcm.device if master_vcm else None
@python_2_unicode_compatible
class VCMembership(models.Model):
"""
An attachment of a physical Device to a VirtualChassis.
"""
virtual_chassis = models.ForeignKey(
to='VirtualChassis',
on_delete=models.CASCADE,
related_name='memberships'
)
device = models.OneToOneField(
to='Device',
on_delete=models.CASCADE,
related_name='vc_membership'
)
position = models.PositiveSmallIntegerField(
validators=[MaxValueValidator(255)]
)
is_master = models.BooleanField(
default=False
)
priority = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[MaxValueValidator(255)]
)
class Meta:
ordering = ['virtual_chassis', 'position']
unique_together = ['virtual_chassis', 'position']
verbose_name = 'VC membership'
def __str__(self):
return self.device.name
def clean(self):
# We have to call this here because it won't be called by VCMembershipForm
self.validate_unique()
# Check for master conflicts
if getattr(self, 'virtual_chassis', None) and self.is_master:
master_conflict = VCMembership.objects.filter(
virtual_chassis=self.virtual_chassis, is_master=True
).exclude(pk=self.pk).first()
if master_conflict:
raise ValidationError(
"{} has already been designated as the master for this virtual chassis. It must be demoted before "
"a new master can be assigned.".format(master_conflict.device)
)

View File

@ -1,17 +0,0 @@
from __future__ import unicode_literals
from django.db.models.signals import post_delete
from django.dispatch import receiver
from .models import VCMembership
@receiver(post_delete, sender=VCMembership)
def delete_empty_vc(instance, **kwargs):
"""
When the last VCMembership of a VirtualChassis has been deleted, delete the VirtualChassis as well.
"""
pass
# virtual_chassis = instance.virtual_chassis
# if not VCMembership.objects.filter(virtual_chassis=virtual_chassis).exists():
# virtual_chassis.delete()

View File

@ -10,7 +10,7 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VCMembership, VirtualChassis, RackReservation, RackRole, Region, Site, VirtualChassis,
) )
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from users.models import Token from users.models import Token
@ -2937,227 +2937,227 @@ class VirtualChassisTest(HttpStatusMixin, APITestCase):
self.assertEqual(VirtualChassis.objects.count(), 2) self.assertEqual(VirtualChassis.objects.count(), 2)
class VCMembershipTest(HttpStatusMixin, APITestCase): # class VCMembershipTest(HttpStatusMixin, APITestCase):
#
def setUp(self): # def setUp(self):
#
user = User.objects.create(username='testuser', is_superuser=True) # user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user) # token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)} # self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
#
site = Site.objects.create(name='Test Site', slug='test-site') # site = Site.objects.create(name='Test Site', slug='test-site')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer') # manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')
device_type = DeviceType.objects.create( # device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type', slug='test-device-type' # manufacturer=manufacturer, model='Test Device Type', slug='test-device-type'
) # )
device_role = DeviceRole.objects.create( # device_role = DeviceRole.objects.create(
name='Test Device Role', slug='test-device-role', color='ff0000' # name='Test Device Role', slug='test-device-role', color='ff0000'
) # )
#
# Create 9 member Devices with 12 interfaces each # # Create 9 member Devices with 12 interfaces each
self.device1 = Device.objects.create( # self.device1 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch1', site=site # device_type=device_type, device_role=device_role, name='StackSwitch1', site=site
) # )
self.device2 = Device.objects.create( # self.device2 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch2', site=site # device_type=device_type, device_role=device_role, name='StackSwitch2', site=site
) # )
self.device3 = Device.objects.create( # self.device3 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch3', site=site # device_type=device_type, device_role=device_role, name='StackSwitch3', site=site
) # )
self.device4 = Device.objects.create( # self.device4 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch4', site=site # device_type=device_type, device_role=device_role, name='StackSwitch4', site=site
) # )
self.device5 = Device.objects.create( # self.device5 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch5', site=site # device_type=device_type, device_role=device_role, name='StackSwitch5', site=site
) # )
self.device6 = Device.objects.create( # self.device6 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch6', site=site # device_type=device_type, device_role=device_role, name='StackSwitch6', site=site
) # )
self.device7 = Device.objects.create( # self.device7 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch7', site=site # device_type=device_type, device_role=device_role, name='StackSwitch7', site=site
) # )
self.device8 = Device.objects.create( # self.device8 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch8', site=site # device_type=device_type, device_role=device_role, name='StackSwitch8', site=site
) # )
self.device9 = Device.objects.create( # self.device9 = Device.objects.create(
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site # device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
) # )
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device1, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device1, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device2, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device2, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device3, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device3, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device4, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device4, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device5, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device5, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device6, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device6, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device7, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device7, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device8, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device8, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13): # for i in range(0, 13):
Interface.objects.create(device=self.device9, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED) # Interface.objects.create(device=self.device9, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
#
# Create two VirtualChassis with three members each # # Create two VirtualChassis with three members each
self.vc1 = VirtualChassis.objects.create(domain='test-domain-1') # self.vc1 = VirtualChassis.objects.create(domain='test-domain-1')
self.vc2 = VirtualChassis.objects.create(domain='test-domain-2') # self.vc2 = VirtualChassis.objects.create(domain='test-domain-2')
self.vcm1 = VCMembership.objects.create( # self.vcm1 = VCMembership.objects.create(
virtual_chassis=self.vc1, device=self.device1, position=1, priority=10, is_master=True # virtual_chassis=self.vc1, device=self.device1, position=1, priority=10, is_master=True
) # )
self.vcm2 = VCMembership.objects.create( # self.vcm2 = VCMembership.objects.create(
virtual_chassis=self.vc1, device=self.device2, position=2, priority=20 # virtual_chassis=self.vc1, device=self.device2, position=2, priority=20
) # )
self.vcm3 = VCMembership.objects.create( # self.vcm3 = VCMembership.objects.create(
virtual_chassis=self.vc1, device=self.device3, position=3, priority=30 # virtual_chassis=self.vc1, device=self.device3, position=3, priority=30
) # )
self.vcm4 = VCMembership.objects.create( # self.vcm4 = VCMembership.objects.create(
virtual_chassis=self.vc2, device=self.device4, position=1, priority=10, is_master=True # virtual_chassis=self.vc2, device=self.device4, position=1, priority=10, is_master=True
) # )
self.vcm5 = VCMembership.objects.create( # self.vcm5 = VCMembership.objects.create(
virtual_chassis=self.vc2, device=self.device5, position=2, priority=20 # virtual_chassis=self.vc2, device=self.device5, position=2, priority=20
) # )
self.vcm6 = VCMembership.objects.create( # self.vcm6 = VCMembership.objects.create(
virtual_chassis=self.vc2, device=self.device6, position=3, priority=30 # virtual_chassis=self.vc2, device=self.device6, position=3, priority=30
) # )
#
def test_get_vcmembership(self): # def test_get_vcmembership(self):
#
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm1.pk}) # url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm1.pk})
response = self.client.get(url, **self.header) # response = self.client.get(url, **self.header)
#
self.assertEqual(response.data['virtual_chassis']['id'], self.vc1.pk) # self.assertEqual(response.data['virtual_chassis']['id'], self.vc1.pk)
self.assertEqual(response.data['device']['id'], self.device1.pk) # self.assertEqual(response.data['device']['id'], self.device1.pk)
self.assertEqual(response.data['position'], 1) # self.assertEqual(response.data['position'], 1)
self.assertEqual(response.data['is_master'], True) # self.assertEqual(response.data['is_master'], True)
self.assertEqual(response.data['priority'], 10) # self.assertEqual(response.data['priority'], 10)
#
def test_list_vcmemberships(self): # def test_list_vcmemberships(self):
#
url = reverse('dcim-api:vcmembership-list') # url = reverse('dcim-api:vcmembership-list')
response = self.client.get(url, **self.header) # response = self.client.get(url, **self.header)
#
self.assertEqual(response.data['count'], 6) # self.assertEqual(response.data['count'], 6)
#
def test_create_vcmembership(self): # def test_create_vcmembership(self):
#
url = reverse('dcim-api:vcmembership-list') # url = reverse('dcim-api:vcmembership-list')
#
# Try creating the first membership without is_master. This should fail. # # Try creating the first membership without is_master. This should fail.
data = { # data = {
'device': self.device7.pk, # 'device': self.device7.pk,
'position': 1, # 'position': 1,
'priority': 10, # 'priority': 10,
} # }
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) # self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
#
# Add is_master=True and try again. This should succeed. # # Add is_master=True and try again. This should succeed.
data.update({ # data.update({
'is_master': True, # 'is_master': True,
}) # })
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) # self.assertHttpStatus(response, status.HTTP_201_CREATED)
virtualchassis_id = VirtualChassis.objects.get(pk=response.data['virtual_chassis']).pk # virtualchassis_id = VirtualChassis.objects.get(pk=response.data['virtual_chassis']).pk
#
# Try adding a second member with the same position # # Try adding a second member with the same position
data = { # data = {
'virtual_chassis': virtualchassis_id, # 'virtual_chassis': virtualchassis_id,
'device': self.device8.pk, # 'device': self.device8.pk,
'position': 1, # 'position': 1,
'priority': 20, # 'priority': 20,
} # }
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) # self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
#
# Try adding a second member with is_master=True # # Try adding a second member with is_master=True
data['is_master'] = True # data['is_master'] = True
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) # self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
#
# Add a second member (valid) # # Add a second member (valid)
del(data['is_master']) # del(data['is_master'])
data['position'] = 2 # data['position'] = 2
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) # self.assertHttpStatus(response, status.HTTP_201_CREATED)
#
# Add a third member (valid) # # Add a third member (valid)
data = { # data = {
'virtual_chassis': virtualchassis_id, # 'virtual_chassis': virtualchassis_id,
'device': self.device9.pk, # 'device': self.device9.pk,
'position': 3, # 'position': 3,
'priority': 30, # 'priority': 30,
} # }
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) # self.assertHttpStatus(response, status.HTTP_201_CREATED)
#
self.assertEqual(VCMembership.objects.count(), 9) # self.assertEqual(VCMembership.objects.count(), 9)
#
def test_create_vcmembership_bulk(self): # def test_create_vcmembership_bulk(self):
#
vc3 = VirtualChassis.objects.create() # vc3 = VirtualChassis.objects.create()
#
data = [ # data = [
# Set the master of an existing VC # # Set the master of an existing VC
{ # {
'virtual_chassis': vc3.pk, # 'virtual_chassis': vc3.pk,
'device': self.device7.pk, # 'device': self.device7.pk,
'position': 1, # 'position': 1,
'is_master': True, # 'is_master': True,
'priority': 10, # 'priority': 10,
}, # },
# Add a non-master member to a VC # # Add a non-master member to a VC
{ # {
'virtual_chassis': vc3.pk, # 'virtual_chassis': vc3.pk,
'device': self.device8.pk, # 'device': self.device8.pk,
'position': 2, # 'position': 2,
'is_master': False, # 'is_master': False,
'priority': 20, # 'priority': 20,
}, # },
# Force the creation of a new VC # # Force the creation of a new VC
{ # {
'device': self.device9.pk, # 'device': self.device9.pk,
'position': 1, # 'position': 1,
'is_master': True, # 'is_master': True,
'priority': 10, # 'priority': 10,
}, # },
] # ]
#
url = reverse('dcim-api:vcmembership-list') # url = reverse('dcim-api:vcmembership-list')
response = self.client.post(url, data, format='json', **self.header) # response = self.client.post(url, data, format='json', **self.header)
#
self.assertHttpStatus(response, status.HTTP_201_CREATED) # self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(VirtualChassis.objects.count(), 4) # self.assertEqual(VirtualChassis.objects.count(), 4)
self.assertEqual(VCMembership.objects.count(), 9) # self.assertEqual(VCMembership.objects.count(), 9)
self.assertEqual(response.data[0]['device'], data[0]['device']) # self.assertEqual(response.data[0]['device'], data[0]['device'])
self.assertEqual(response.data[1]['device'], data[1]['device']) # self.assertEqual(response.data[1]['device'], data[1]['device'])
self.assertEqual(response.data[2]['device'], data[2]['device']) # self.assertEqual(response.data[2]['device'], data[2]['device'])
#
def test_update_vcmembership(self): # def test_update_vcmembership(self):
#
data = { # data = {
'virtual_chassis': self.vc2.pk, # 'virtual_chassis': self.vc2.pk,
'device': self.device7.pk, # 'device': self.device7.pk,
'position': 9, # 'position': 9,
'priority': 90, # 'priority': 90,
} # }
#
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk}) # url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
response = self.client.put(url, data, format='json', **self.header) # response = self.client.put(url, data, format='json', **self.header)
#
self.assertHttpStatus(response, status.HTTP_200_OK) # self.assertHttpStatus(response, status.HTTP_200_OK)
vcm3 = VCMembership.objects.get(pk=response.data['id']) # vcm3 = VCMembership.objects.get(pk=response.data['id'])
self.assertEqual(vcm3.virtual_chassis.pk, data['virtual_chassis']) # self.assertEqual(vcm3.virtual_chassis.pk, data['virtual_chassis'])
self.assertEqual(vcm3.device.pk, data['device']) # self.assertEqual(vcm3.device.pk, data['device'])
self.assertEqual(vcm3.position, data['position']) # self.assertEqual(vcm3.position, data['position'])
self.assertEqual(vcm3.priority, data['priority']) # self.assertEqual(vcm3.priority, data['priority'])
#
def test_delete_vcmembership(self): # def test_delete_vcmembership(self):
#
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk}) # url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
response = self.client.delete(url, **self.header) # response = self.client.delete(url, **self.header)
#
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) # self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(VCMembership.objects.count(), 5) # self.assertEqual(VCMembership.objects.count(), 5)

View File

@ -220,10 +220,6 @@ urlpatterns = [
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), # url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
# VC memberships
url(r'^vc-memberships/(?P<pk>\d+)/edit/$', views.VCMembershipEditView.as_view(), name='vcmembership_edit'),
url(r'^vc-memberships/(?P<pk>\d+)/delete/$', views.VCMembershipDeleteView.as_view(), name='vcmembership_delete'),
] ]

View File

@ -7,7 +7,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction from django.db import transaction
from django.db.models import Count, Q from django.db.models import Count, Q
from django.forms import ModelChoiceField, modelformset_factory from django.forms import ModelChoiceField, ModelForm, modelformset_factory
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
@ -33,7 +33,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VCMembership, VirtualChassis, RackReservation, RackRole, Region, Site, VirtualChassis,
) )
@ -861,8 +861,11 @@ class DeviceView(View):
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
), pk=pk) ), pk=pk)
# Find virtual chassis memberships # VirtualChassis members
vc_memberships = VCMembership.objects.filter(virtual_chassis=device.virtual_chassis).select_related('device') if device.virtual_chassis is not None:
vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis)
else:
vc_members = []
# Console ports # Console ports
console_ports = natsorted( console_ports = natsorted(
@ -922,7 +925,7 @@ class DeviceView(View):
'device_bays': device_bays, 'device_bays': device_bays,
'services': services, 'services': services,
'secrets': secrets, 'secrets': secrets,
'vc_memberships': vc_memberships, 'vc_members': vc_members,
'related_devices': related_devices, 'related_devices': related_devices,
'show_graphs': show_graphs, 'show_graphs': show_graphs,
}) })
@ -2039,155 +2042,6 @@ class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = InventoryItem model = InventoryItem
#
# Virtual chassis
#
class VirtualChassisListView(ObjectListView):
queryset = VirtualChassis.objects.annotate(member_count=Count('memberships'))
table = tables.VirtualChassisTable
template_name = 'dcim/virtualchassis_list.html'
class VirtualChassisCreateView(PermissionRequiredMixin, View):
permission_required = 'dcim.add_virtualchassis'
def post(self, request):
# Get the list of devices being added to a VirtualChassis
pk_form = forms.DeviceSelectionForm(request.POST)
pk_form.full_clean()
device_list = pk_form.cleaned_data.get('pk')
if not device_list:
messages.warning(request, "No devices were selected.")
return redirect('dcim:device_list')
# Generate a custom VCMembershipForm where the device field is limited to only the selected devices
class _VCMembershipForm(forms.VCMembershipForm):
device = ModelChoiceField(queryset=Device.objects.filter(pk__in=device_list))
class Meta:
model = VCMembership
fields = ['device', 'position', 'priority']
VCMembershipFormSet = modelformset_factory(model=VCMembership, form=_VCMembershipForm, extra=len(device_list))
if '_create' in request.POST:
vc_form = forms.VirtualChassisCreateForm(device_list, request.POST)
formset = VCMembershipFormSet(request.POST)
if vc_form.is_valid() and formset.is_valid():
with transaction.atomic():
virtual_chassis = vc_form.save()
vc_memberships = formset.save(commit=False)
for vcm in vc_memberships:
vcm.virtual_chassis = virtual_chassis
if vcm.device == vc_form.cleaned_data['master']:
vcm.is_master = True
vcm.save()
return redirect(vc_form.cleaned_data['master'].get_absolute_url())
else:
vc_form = forms.VirtualChassisCreateForm(device_list)
initial_data = [{'device': pk, 'position': i} for i, pk in enumerate(device_list, start=1)]
formset = VCMembershipFormSet(queryset=VCMembership.objects.none(), initial=initial_data)
return render(request, 'dcim/virtualchassis_add.html', {
'pk_form': pk_form,
'vc_form': vc_form,
'formset': formset,
'return_url': reverse('dcim:device_list'),
})
class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_virtualchassis'
model = VirtualChassis
model_form = forms.VirtualChassisForm
template_name = 'dcim/virtualchassis_edit.html'
class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_virtualchassis'
model = VirtualChassis
default_return_url = 'dcim:device_list'
class VirtualChassisAddMemberView(GetReturnURLMixin, View):
"""
Create a new VCMembership tying a Device to the VirtualChassis.
"""
template_name = 'utilities/obj_edit.html'
def get(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
obj = VCMembership(virtual_chassis=virtual_chassis)
initial_data = {k: request.GET[k] for k in request.GET}
form = forms.VCMembershipCreateForm(instance=obj, initial=initial_data)
return render(request, self.template_name, {
'obj': obj,
'obj_type': VCMembership._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
})
def post(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
obj = VCMembership(virtual_chassis=virtual_chassis)
form = forms.VCMembershipCreateForm(request.POST, instance=obj)
if form.is_valid():
obj = form.save()
msg = 'Added member <a href="{}">{}</a>'.format(obj.device.get_absolute_url(), escape(obj.device))
messages.success(request, mark_safe(msg))
UserAction.objects.log_create(request.user, obj, msg)
if '_addanother' in request.POST:
return redirect(request.get_full_path())
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, {
'obj': obj,
'obj_type': VCMembership._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
})
#
# VC memberships
#
class VCMembershipEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_vcmembership'
model = VCMembership
model_form = forms.VCMembershipForm
class VCMembershipDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_vcmembership'
model = VCMembership
parent_field = 'device'
def get_return_url(self, request, obj):
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView): class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_inventoryitem' permission_required = 'dcim.add_inventoryitem'
model_form = forms.InventoryItemCSVForm model_form = forms.InventoryItemCSVForm
@ -2212,3 +2066,106 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html' template_name = 'dcim/inventoryitem_bulk_delete.html'
default_return_url = 'dcim:inventoryitem_list' default_return_url = 'dcim:inventoryitem_list'
#
# Virtual chassis
#
class VirtualChassisListView(ObjectListView):
queryset = VirtualChassis.objects.annotate(member_count=Count('members'))
table = tables.VirtualChassisTable
template_name = 'dcim/virtualchassis_list.html'
class VirtualChassisCreateView(PermissionRequiredMixin, View):
permission_required = 'dcim.add_virtualchassis'
def post(self, request):
# Get the list of devices being added to a VirtualChassis
pk_form = forms.DeviceSelectionForm(request.POST)
pk_form.full_clean()
device_list = pk_form.cleaned_data.get('pk')
if not device_list:
messages.warning(request, "No devices were selected.")
return redirect('dcim:device_list')
# TODO: Error if any of the devices already belong to a VC
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
if '_create' in request.POST:
vc_form = forms.VirtualChassisForm(request.POST)
formset = VCMemberFormSet(request.POST)
if vc_form.is_valid() and formset.is_valid():
with transaction.atomic():
virtual_chassis = vc_form.save()
devices = formset.save(commit=False)
for device in devices:
device.virtual_chassis = virtual_chassis
device.save()
return redirect(vc_form.cleaned_data['master'].get_absolute_url())
else:
vc_form = forms.VirtualChassisForm()
vc_form.fields['master'].queryset = Device.objects.filter(pk__in=device_list)
formset = VCMemberFormSet(queryset=Device.objects.filter(pk__in=device_list))
return render(request, 'dcim/virtualchassis_edit.html', {
'pk_form': pk_form,
'vc_form': vc_form,
'formset': formset,
'return_url': reverse('dcim:device_list'),
})
class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
permission_required = 'dcim.change_virtualchassis'
def get(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
vc_form.fields['master'].queryset = virtual_chassis.members.all()
formset = VCMemberFormSet(queryset=virtual_chassis.members.all())
return render(request, 'dcim/virtualchassis_edit.html', {
'vc_form': vc_form,
'formset': formset,
'return_url': self.get_return_url(request, virtual_chassis),
})
def post(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
vc_form.fields['master'].queryset = virtual_chassis.members.all()
formset = VCMemberFormSet(request.POST, queryset=virtual_chassis.members.all())
if vc_form.is_valid() and formset.is_valid():
vc_form.save()
formset.save()
return redirect(vc_form.cleaned_data['master'].get_absolute_url())
return render(request, 'dcim/virtualchassis_add.html', {
'vc_form': vc_form,
'formset': formset,
'return_url': self.get_return_url(request, virtual_chassis),
})
class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_virtualchassis'
model = VirtualChassis
default_return_url = 'dcim:device_list'

View File

@ -98,7 +98,7 @@
</tr> </tr>
</table> </table>
</div> </div>
{% if vc_memberships %} {% if vc_members %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Virtual Chassis</strong> <strong>Virtual Chassis</strong>
@ -110,24 +110,22 @@
<th>Master</th> <th>Master</th>
<th>Priority</th> <th>Priority</th>
</tr> </tr>
{% for vcm in vc_memberships %} {% for vc_member in vc_members %}
<tr{% if vcm.device == device %} class="success"{% endif %}> <tr{% if vc_member.device == device %} class="success"{% endif %}>
<td> <td>
<a href="{{ vcm.device.get_absolute_url }}">{{ vcm.device }}</a> <a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
</td> </td>
<td>{{ vcm.position }}</td> <td>{{ vc_member.vc_position }}</td>
<td>{% if vcm.is_master %}<i class="fa fa-check"></i>{% endif %}</td> <td>{% if device.virtual_chassis.master == vc_member %}<i class="fa fa-check"></i>{% endif %}</td>
<td>{{ vcm.priority|default:"" }}</td> <td>{{ vc_member.vc_priority|default:"" }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class="panel-footer text-right"> <div class="panel-footer text-right">
{% if perms.dcim.add_vcmembership %} {% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_add_member' pk=device.virtual_chassis.pk %}?site={{ device.site.pk }}&rack={{ device.rack.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:virtualchassis_add_member' pk=device.virtual_chassis.pk %}?site={{ device.site.pk }}&rack={{ device.rack.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member
</a> </a>
{% endif %}
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=device.virtual_chassis.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <a href="{% url 'dcim:virtualchassis_edit' pk=device.virtual_chassis.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Virtual Chassis <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Virtual Chassis
</a> </a>

View File

@ -1,56 +0,0 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block content %}
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
{% csrf_token %}
{{ pk_form.pk }}
{{ formset.management_form }}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>{% block title %}New Virtual Chassis{% endblock %}</h3>
{% if vc_form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ vc_form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Virtual Chassis</strong></div>
<div class="table panel-body">
{% render_form vc_form %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Members</strong></div>
<table class="table panel-body">
<thead>
<tr>
<th>Device</th>
<th>Position</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>{{ form.device }}</td>
<td>{{ form.position }}</td>
<td>{{ form.priority }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
{% endblock %}

View File

@ -1,44 +1,59 @@
{% extends 'utilities/obj_edit.html' %} {% extends '_base.html' %}
{% load form_helpers %} {% load form_helpers %}
{% block content %} {% block content %}
{{ block.super }} <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
<div class="row"> {% csrf_token %}
<div class="col-md-6 col-md-offset-3"> {{ pk_form.pk }}
<h3>Memberships</h3> {{ formset.management_form }}
<div class="panel panel-default"> <div class="row">
<table class="table panel-body"> <div class="col-md-6 col-md-offset-3">
<tr class="table-headings"> <h3>{% block title %}{% if vc_form.instance %}Editing {{ vc_form.instance }}{% else %}New Virtual Chassis{% endif %}{% endblock %}</h3>
<th>Device</th> {% if vc_form.non_field_errors %}
<th>Position</th> <div class="panel panel-danger">
<th>Master</th> <div class="panel-heading"><strong>Errors</strong></div>
<th>Priority</th> <div class="panel-body">
<th></th> {{ vc_form.non_field_errors }}
</tr> </div>
{% for vcm in form.instance.memberships.all %} </div>
<tr> {% endif %}
<td> <div class="panel panel-default">
<a href="{{ vcm.device.get_absolute_url }}">{{ vcm.device }}</a> <div class="panel-heading"><strong>Virtual Chassis</strong></div>
</td> <div class="table panel-body">
<td>{{ vcm.position }}</td> {% render_form vc_form %}
<td>{% if vcm.is_master %}<i class="fa fa-check"></i>{% endif %}</td> </div>
<td>{{ vcm.priority|default:"" }}</td> </div>
<td class="text-right"> <div class="panel panel-default">
{% if perms.dcim.change_vcmembership %} <div class="panel-heading"><strong>Members</strong></div>
<a href="{% url 'dcim:vcmembership_edit' pk=vcm.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=vcm.virtual_chassis.pk %}" class="btn btn-warning btn-xs"> <table class="table panel-body">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <thead>
</a> <tr>
{% endif %} <th>Device</th>
{% if perms.dcim.delete_vcmembership %} <th>Position</th>
<a href="{% url 'dcim:vcmembership_delete' pk=vcm.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=vcm.virtual_chassis.pk %}" class="btn btn-danger btn-xs"> <th>Priority</th>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete </tr>
</a> </thead>
{% endif %} <tbody>
</td> {% for form in formset %}
</tr> {% for hidden in form.hidden_fields %}
{% endfor %} {{ hidden }}
</table> {% endfor %}
<tr>
<td>{{ form.instance.name }}</td>
<td>{{ form.vc_position }}</td>
<td>{{ form.vc_priority }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> <div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
{% endblock %} {% endblock %}