1
0
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:
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,
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']

View File

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

View File

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

View File

@ -6,6 +6,3 @@ from django.apps import AppConfig
class DCIMConfig(AppConfig):
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,
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',

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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 %}
{% 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 %}