mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Collapsed VCMembership into the Device model (WIP)
This commit is contained in:
@ -14,7 +14,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
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 ipam.models import IPAddress, VLAN
|
||||
@ -489,7 +489,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
primary_ip4 = DeviceIPAddressSerializer()
|
||||
primary_ip6 = DeviceIPAddressSerializer()
|
||||
parent_device = serializers.SerializerMethodField()
|
||||
virtual_chassis = serializers.SerializerMethodField()
|
||||
cluster = NestedClusterSerializer()
|
||||
|
||||
class Meta:
|
||||
@ -497,7 +496,8 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
fields = [
|
||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'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):
|
||||
@ -510,16 +510,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).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):
|
||||
|
||||
@ -833,10 +823,11 @@ class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(serializers.ModelSerializer):
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'domain']
|
||||
fields = ['id', 'master', 'domain']
|
||||
|
||||
|
||||
class NestedVirtualChassisSerializer(serializers.ModelSerializer):
|
||||
@ -851,44 +842,4 @@ class WritableVirtualChassisSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', '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
|
||||
fields = ['id', 'master', 'domain']
|
||||
|
@ -62,7 +62,6 @@ router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||
|
||||
# Virtual chassis
|
||||
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||
router.register(r'vc-memberships', views.VCMembershipViewSet)
|
||||
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||
|
@ -16,7 +16,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
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.views import CustomFieldModelViewSet
|
||||
@ -403,32 +403,6 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
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
|
||||
#
|
||||
|
@ -6,6 +6,3 @@ from django.apps import AppConfig
|
||||
class DCIMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
||||
|
||||
def ready(self):
|
||||
import dcim.signals
|
||||
|
@ -17,7 +17,7 @@ from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
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']
|
||||
|
||||
|
||||
class VCMembershipFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VCMembership
|
||||
fields = ['virtual_chassis', 'device', 'position', 'is_master', 'priority']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
|
@ -32,7 +32,7 @@ from .models import (
|
||||
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||
RackRole, Region, Site, VCMembership, VirtualChassis
|
||||
RackRole, Region, Site, VirtualChassis
|
||||
)
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
@ -2265,94 +2265,49 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||
# 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):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
|
||||
master = forms.ModelChoiceField(queryset=Device.objects.all())
|
||||
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
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)
|
||||
|
||||
|
||||
#
|
||||
# VC memberships
|
||||
#
|
||||
|
||||
class VCMembershipForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VCMembership
|
||||
fields = ['position', 'priority']
|
||||
|
||||
|
||||
class VCMembershipCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
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']
|
||||
# class VCAddMemberForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
# site = forms.ModelChoiceField(
|
||||
# queryset=Site.objects.all(),
|
||||
# label='Site',
|
||||
# required=False,
|
||||
# widget=forms.Select(
|
||||
# attrs={'filter-for': 'rack'}
|
||||
# )
|
||||
# )
|
||||
# rack = ChainedModelChoiceField(
|
||||
# queryset=Rack.objects.all(),
|
||||
# chains=(
|
||||
# ('site', 'site'),
|
||||
# ),
|
||||
# label='Rack',
|
||||
# required=False,
|
||||
# widget=APISelect(
|
||||
# api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
# attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
# )
|
||||
# )
|
||||
# device = ChainedModelChoiceField(
|
||||
# queryset=Device.objects.all(),
|
||||
# chains=(
|
||||
# ('site', 'site'),
|
||||
# ('rack', 'rack'),
|
||||
# ),
|
||||
# label='Device',
|
||||
# widget=APISelect(
|
||||
# api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
# display_field='display_name'
|
||||
# )
|
||||
# )
|
||||
# vc_position = forms.IntegerField(label='Position')
|
||||
# vc_priority = forms.IntegerField(required=False, label='Priority')
|
||||
|
@ -14,34 +14,31 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
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(
|
||||
name='VirtualChassis',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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(
|
||||
model_name='vcmembership',
|
||||
model_name='device',
|
||||
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(
|
||||
name='vcmembership',
|
||||
unique_together=set([('virtual_chassis', 'position')]),
|
||||
name='device',
|
||||
unique_together=set([('virtual_chassis', 'vc_position'), ('rack', 'position', 'face')]),
|
||||
),
|
||||
]
|
||||
|
@ -867,6 +867,23 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
blank=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)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
images = GenericRelation(ImageAttachment)
|
||||
@ -880,7 +897,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
unique_together = [
|
||||
['rack', 'position', 'face'],
|
||||
['virtual_chassis', 'vc_position'],
|
||||
]
|
||||
permissions = (
|
||||
('napalm_read', 'Read-only access to devices via NAPALM'),
|
||||
('napalm_write', 'Read/write access to devices via NAPALM'),
|
||||
@ -1079,13 +1099,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def virtual_chassis(self):
|
||||
try:
|
||||
return VCMembership.objects.get(device=self).virtual_chassis
|
||||
except VCMembership.DoesNotExist:
|
||||
return None
|
||||
|
||||
@property
|
||||
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).
|
||||
"""
|
||||
master = models.OneToOneField(
|
||||
to='Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vc_master_for'
|
||||
)
|
||||
domain = models.CharField(
|
||||
max_length=30,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.master.name
|
||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
||||
|
||||
def get_absolute_url(self):
|
||||
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)
|
||||
)
|
||||
|
@ -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()
|
@ -10,7 +10,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
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 users.models import Token
|
||||
@ -2937,227 +2937,227 @@ class VirtualChassisTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(VirtualChassis.objects.count(), 2)
|
||||
|
||||
|
||||
class VCMembershipTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type', slug='test-device-type'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Test Device Role', slug='test-device-role', color='ff0000'
|
||||
)
|
||||
|
||||
# Create 9 member Devices with 12 interfaces each
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch1', site=site
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch2', site=site
|
||||
)
|
||||
self.device3 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch3', site=site
|
||||
)
|
||||
self.device4 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch4', site=site
|
||||
)
|
||||
self.device5 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch5', site=site
|
||||
)
|
||||
self.device6 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch6', site=site
|
||||
)
|
||||
self.device7 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch7', site=site
|
||||
)
|
||||
self.device8 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch8', site=site
|
||||
)
|
||||
self.device9 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
|
||||
)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device1, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device2, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device3, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device4, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device5, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device6, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device7, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device8, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device9, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
|
||||
# Create two VirtualChassis with three members each
|
||||
self.vc1 = VirtualChassis.objects.create(domain='test-domain-1')
|
||||
self.vc2 = VirtualChassis.objects.create(domain='test-domain-2')
|
||||
self.vcm1 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc1, device=self.device1, position=1, priority=10, is_master=True
|
||||
)
|
||||
self.vcm2 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc1, device=self.device2, position=2, priority=20
|
||||
)
|
||||
self.vcm3 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc1, device=self.device3, position=3, priority=30
|
||||
)
|
||||
self.vcm4 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc2, device=self.device4, position=1, priority=10, is_master=True
|
||||
)
|
||||
self.vcm5 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc2, device=self.device5, position=2, priority=20
|
||||
)
|
||||
self.vcm6 = VCMembership.objects.create(
|
||||
virtual_chassis=self.vc2, device=self.device6, position=3, priority=30
|
||||
)
|
||||
|
||||
def test_get_vcmembership(self):
|
||||
|
||||
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['virtual_chassis']['id'], self.vc1.pk)
|
||||
self.assertEqual(response.data['device']['id'], self.device1.pk)
|
||||
self.assertEqual(response.data['position'], 1)
|
||||
self.assertEqual(response.data['is_master'], True)
|
||||
self.assertEqual(response.data['priority'], 10)
|
||||
|
||||
def test_list_vcmemberships(self):
|
||||
|
||||
url = reverse('dcim-api:vcmembership-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 6)
|
||||
|
||||
def test_create_vcmembership(self):
|
||||
|
||||
url = reverse('dcim-api:vcmembership-list')
|
||||
|
||||
# Try creating the first membership without is_master. This should fail.
|
||||
data = {
|
||||
'device': self.device7.pk,
|
||||
'position': 1,
|
||||
'priority': 10,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Add is_master=True and try again. This should succeed.
|
||||
data.update({
|
||||
'is_master': True,
|
||||
})
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
virtualchassis_id = VirtualChassis.objects.get(pk=response.data['virtual_chassis']).pk
|
||||
|
||||
# Try adding a second member with the same position
|
||||
data = {
|
||||
'virtual_chassis': virtualchassis_id,
|
||||
'device': self.device8.pk,
|
||||
'position': 1,
|
||||
'priority': 20,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Try adding a second member with is_master=True
|
||||
data['is_master'] = True
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Add a second member (valid)
|
||||
del(data['is_master'])
|
||||
data['position'] = 2
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
# Add a third member (valid)
|
||||
data = {
|
||||
'virtual_chassis': virtualchassis_id,
|
||||
'device': self.device9.pk,
|
||||
'position': 3,
|
||||
'priority': 30,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
self.assertEqual(VCMembership.objects.count(), 9)
|
||||
|
||||
def test_create_vcmembership_bulk(self):
|
||||
|
||||
vc3 = VirtualChassis.objects.create()
|
||||
|
||||
data = [
|
||||
# Set the master of an existing VC
|
||||
{
|
||||
'virtual_chassis': vc3.pk,
|
||||
'device': self.device7.pk,
|
||||
'position': 1,
|
||||
'is_master': True,
|
||||
'priority': 10,
|
||||
},
|
||||
# Add a non-master member to a VC
|
||||
{
|
||||
'virtual_chassis': vc3.pk,
|
||||
'device': self.device8.pk,
|
||||
'position': 2,
|
||||
'is_master': False,
|
||||
'priority': 20,
|
||||
},
|
||||
# Force the creation of a new VC
|
||||
{
|
||||
'device': self.device9.pk,
|
||||
'position': 1,
|
||||
'is_master': True,
|
||||
'priority': 10,
|
||||
},
|
||||
]
|
||||
|
||||
url = reverse('dcim-api:vcmembership-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(VirtualChassis.objects.count(), 4)
|
||||
self.assertEqual(VCMembership.objects.count(), 9)
|
||||
self.assertEqual(response.data[0]['device'], data[0]['device'])
|
||||
self.assertEqual(response.data[1]['device'], data[1]['device'])
|
||||
self.assertEqual(response.data[2]['device'], data[2]['device'])
|
||||
|
||||
def test_update_vcmembership(self):
|
||||
|
||||
data = {
|
||||
'virtual_chassis': self.vc2.pk,
|
||||
'device': self.device7.pk,
|
||||
'position': 9,
|
||||
'priority': 90,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
vcm3 = VCMembership.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vcm3.virtual_chassis.pk, data['virtual_chassis'])
|
||||
self.assertEqual(vcm3.device.pk, data['device'])
|
||||
self.assertEqual(vcm3.position, data['position'])
|
||||
self.assertEqual(vcm3.priority, data['priority'])
|
||||
|
||||
def test_delete_vcmembership(self):
|
||||
|
||||
url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(VCMembership.objects.count(), 5)
|
||||
# class VCMembershipTest(HttpStatusMixin, APITestCase):
|
||||
#
|
||||
# def setUp(self):
|
||||
#
|
||||
# user = User.objects.create(username='testuser', is_superuser=True)
|
||||
# token = Token.objects.create(user=user)
|
||||
# self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
#
|
||||
# site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
# manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')
|
||||
# device_type = DeviceType.objects.create(
|
||||
# manufacturer=manufacturer, model='Test Device Type', slug='test-device-type'
|
||||
# )
|
||||
# device_role = DeviceRole.objects.create(
|
||||
# name='Test Device Role', slug='test-device-role', color='ff0000'
|
||||
# )
|
||||
#
|
||||
# # Create 9 member Devices with 12 interfaces each
|
||||
# self.device1 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch1', site=site
|
||||
# )
|
||||
# self.device2 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch2', site=site
|
||||
# )
|
||||
# self.device3 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch3', site=site
|
||||
# )
|
||||
# self.device4 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch4', site=site
|
||||
# )
|
||||
# self.device5 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch5', site=site
|
||||
# )
|
||||
# self.device6 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch6', site=site
|
||||
# )
|
||||
# self.device7 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch7', site=site
|
||||
# )
|
||||
# self.device8 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch8', site=site
|
||||
# )
|
||||
# self.device9 = Device.objects.create(
|
||||
# device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
|
||||
# )
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device1, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device2, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device3, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device4, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device5, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device6, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device7, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device8, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
# for i in range(0, 13):
|
||||
# Interface.objects.create(device=self.device9, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
|
||||
#
|
||||
# # Create two VirtualChassis with three members each
|
||||
# self.vc1 = VirtualChassis.objects.create(domain='test-domain-1')
|
||||
# self.vc2 = VirtualChassis.objects.create(domain='test-domain-2')
|
||||
# self.vcm1 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc1, device=self.device1, position=1, priority=10, is_master=True
|
||||
# )
|
||||
# self.vcm2 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc1, device=self.device2, position=2, priority=20
|
||||
# )
|
||||
# self.vcm3 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc1, device=self.device3, position=3, priority=30
|
||||
# )
|
||||
# self.vcm4 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc2, device=self.device4, position=1, priority=10, is_master=True
|
||||
# )
|
||||
# self.vcm5 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc2, device=self.device5, position=2, priority=20
|
||||
# )
|
||||
# self.vcm6 = VCMembership.objects.create(
|
||||
# virtual_chassis=self.vc2, device=self.device6, position=3, priority=30
|
||||
# )
|
||||
#
|
||||
# def test_get_vcmembership(self):
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm1.pk})
|
||||
# response = self.client.get(url, **self.header)
|
||||
#
|
||||
# self.assertEqual(response.data['virtual_chassis']['id'], self.vc1.pk)
|
||||
# self.assertEqual(response.data['device']['id'], self.device1.pk)
|
||||
# self.assertEqual(response.data['position'], 1)
|
||||
# self.assertEqual(response.data['is_master'], True)
|
||||
# self.assertEqual(response.data['priority'], 10)
|
||||
#
|
||||
# def test_list_vcmemberships(self):
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-list')
|
||||
# response = self.client.get(url, **self.header)
|
||||
#
|
||||
# self.assertEqual(response.data['count'], 6)
|
||||
#
|
||||
# def test_create_vcmembership(self):
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-list')
|
||||
#
|
||||
# # Try creating the first membership without is_master. This should fail.
|
||||
# data = {
|
||||
# 'device': self.device7.pk,
|
||||
# 'position': 1,
|
||||
# 'priority': 10,
|
||||
# }
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
#
|
||||
# # Add is_master=True and try again. This should succeed.
|
||||
# data.update({
|
||||
# 'is_master': True,
|
||||
# })
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
# virtualchassis_id = VirtualChassis.objects.get(pk=response.data['virtual_chassis']).pk
|
||||
#
|
||||
# # Try adding a second member with the same position
|
||||
# data = {
|
||||
# 'virtual_chassis': virtualchassis_id,
|
||||
# 'device': self.device8.pk,
|
||||
# 'position': 1,
|
||||
# 'priority': 20,
|
||||
# }
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
#
|
||||
# # Try adding a second member with is_master=True
|
||||
# data['is_master'] = True
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
#
|
||||
# # Add a second member (valid)
|
||||
# del(data['is_master'])
|
||||
# data['position'] = 2
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
#
|
||||
# # Add a third member (valid)
|
||||
# data = {
|
||||
# 'virtual_chassis': virtualchassis_id,
|
||||
# 'device': self.device9.pk,
|
||||
# 'position': 3,
|
||||
# 'priority': 30,
|
||||
# }
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
# self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
#
|
||||
# self.assertEqual(VCMembership.objects.count(), 9)
|
||||
#
|
||||
# def test_create_vcmembership_bulk(self):
|
||||
#
|
||||
# vc3 = VirtualChassis.objects.create()
|
||||
#
|
||||
# data = [
|
||||
# # Set the master of an existing VC
|
||||
# {
|
||||
# 'virtual_chassis': vc3.pk,
|
||||
# 'device': self.device7.pk,
|
||||
# 'position': 1,
|
||||
# 'is_master': True,
|
||||
# 'priority': 10,
|
||||
# },
|
||||
# # Add a non-master member to a VC
|
||||
# {
|
||||
# 'virtual_chassis': vc3.pk,
|
||||
# 'device': self.device8.pk,
|
||||
# 'position': 2,
|
||||
# 'is_master': False,
|
||||
# 'priority': 20,
|
||||
# },
|
||||
# # Force the creation of a new VC
|
||||
# {
|
||||
# 'device': self.device9.pk,
|
||||
# 'position': 1,
|
||||
# 'is_master': True,
|
||||
# 'priority': 10,
|
||||
# },
|
||||
# ]
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-list')
|
||||
# response = self.client.post(url, data, format='json', **self.header)
|
||||
#
|
||||
# self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
# self.assertEqual(VirtualChassis.objects.count(), 4)
|
||||
# self.assertEqual(VCMembership.objects.count(), 9)
|
||||
# self.assertEqual(response.data[0]['device'], data[0]['device'])
|
||||
# self.assertEqual(response.data[1]['device'], data[1]['device'])
|
||||
# self.assertEqual(response.data[2]['device'], data[2]['device'])
|
||||
#
|
||||
# def test_update_vcmembership(self):
|
||||
#
|
||||
# data = {
|
||||
# 'virtual_chassis': self.vc2.pk,
|
||||
# 'device': self.device7.pk,
|
||||
# 'position': 9,
|
||||
# 'priority': 90,
|
||||
# }
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
|
||||
# response = self.client.put(url, data, format='json', **self.header)
|
||||
#
|
||||
# self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
# vcm3 = VCMembership.objects.get(pk=response.data['id'])
|
||||
# self.assertEqual(vcm3.virtual_chassis.pk, data['virtual_chassis'])
|
||||
# self.assertEqual(vcm3.device.pk, data['device'])
|
||||
# self.assertEqual(vcm3.position, data['position'])
|
||||
# self.assertEqual(vcm3.priority, data['priority'])
|
||||
#
|
||||
# def test_delete_vcmembership(self):
|
||||
#
|
||||
# url = reverse('dcim-api:vcmembership-detail', kwargs={'pk': self.vcm3.pk})
|
||||
# response = self.client.delete(url, **self.header)
|
||||
#
|
||||
# self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
# self.assertEqual(VCMembership.objects.count(), 5)
|
||||
|
@ -220,10 +220,6 @@ urlpatterns = [
|
||||
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+)/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'),
|
||||
|
||||
# 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'),
|
||||
# url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
|
||||
]
|
||||
|
@ -7,7 +7,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db import transaction
|
||||
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.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@ -33,7 +33,7 @@ from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
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'
|
||||
), pk=pk)
|
||||
|
||||
# Find virtual chassis memberships
|
||||
vc_memberships = VCMembership.objects.filter(virtual_chassis=device.virtual_chassis).select_related('device')
|
||||
# VirtualChassis members
|
||||
if device.virtual_chassis is not None:
|
||||
vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis)
|
||||
else:
|
||||
vc_members = []
|
||||
|
||||
# Console ports
|
||||
console_ports = natsorted(
|
||||
@ -922,7 +925,7 @@ class DeviceView(View):
|
||||
'device_bays': device_bays,
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'vc_memberships': vc_memberships,
|
||||
'vc_members': vc_members,
|
||||
'related_devices': related_devices,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
@ -2039,155 +2042,6 @@ class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
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):
|
||||
permission_required = 'dcim.add_inventoryitem'
|
||||
model_form = forms.InventoryItemCSVForm
|
||||
@ -2212,3 +2066,106 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
table = tables.InventoryItemTable
|
||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||
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'
|
||||
|
@ -98,7 +98,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% if vc_memberships %}
|
||||
{% if vc_members %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Virtual Chassis</strong>
|
||||
@ -110,24 +110,22 @@
|
||||
<th>Master</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
{% for vcm in vc_memberships %}
|
||||
<tr{% if vcm.device == device %} class="success"{% endif %}>
|
||||
{% for vc_member in vc_members %}
|
||||
<tr{% if vc_member.device == device %} class="success"{% endif %}>
|
||||
<td>
|
||||
<a href="{{ vcm.device.get_absolute_url }}">{{ vcm.device }}</a>
|
||||
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
|
||||
</td>
|
||||
<td>{{ vcm.position }}</td>
|
||||
<td>{% if vcm.is_master %}<i class="fa fa-check"></i>{% endif %}</td>
|
||||
<td>{{ vcm.priority|default:"" }}</td>
|
||||
<td>{{ vc_member.vc_position }}</td>
|
||||
<td>{% if device.virtual_chassis.master == vc_member %}<i class="fa fa-check"></i>{% endif %}</td>
|
||||
<td>{{ vc_member.vc_priority|default:"" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<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">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member
|
||||
</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">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Virtual Chassis
|
||||
</a>
|
||||
|
@ -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 %}
|
@ -1,44 +1,59 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
{{ block.super }}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>Memberships</h3>
|
||||
<div class="panel panel-default">
|
||||
<table class="table panel-body">
|
||||
<tr class="table-headings">
|
||||
<th>Device</th>
|
||||
<th>Position</th>
|
||||
<th>Master</th>
|
||||
<th>Priority</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for vcm in form.instance.memberships.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ vcm.device.get_absolute_url }}">{{ vcm.device }}</a>
|
||||
</td>
|
||||
<td>{{ vcm.position }}</td>
|
||||
<td>{% if vcm.is_master %}<i class="fa fa-check"></i>{% endif %}</td>
|
||||
<td>{{ vcm.priority|default:"" }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.change_vcmembership %}
|
||||
<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">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_vcmembership %}
|
||||
<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">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<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 %}{% if vc_form.instance %}Editing {{ vc_form.instance }}{% else %}New Virtual Chassis{% endif %}{% 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 %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% 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 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 %}
|
||||
|
Reference in New Issue
Block a user