mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Initial work on #2018: Add name to VirtualChassis
This commit is contained in:
parent
2ac53afd96
commit
59c1e34024
@ -332,7 +332,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.VirtualChassis
|
model = models.VirtualChassis
|
||||||
fields = ['id', 'url', 'master', 'member_count']
|
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -694,12 +694,12 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||||
master = NestedDeviceSerializer()
|
master = NestedDeviceSerializer(required=False)
|
||||||
member_count = serializers.IntegerField(read_only=True)
|
member_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'master', 'domain', 'tags', 'member_count']
|
fields = ['id', 'name', 'domain', 'master', 'tags', 'member_count']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -4114,7 +4114,38 @@ class DeviceSelectionForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
filter_for={
|
||||||
|
'rack': 'site_id',
|
||||||
|
'members': 'site_id',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rack = DynamicModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
filter_for={
|
||||||
|
'members': 'rack_id'
|
||||||
|
},
|
||||||
|
attrs={
|
||||||
|
'nullable': 'true',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
members = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
initial_position = forms.IntegerField(
|
||||||
|
initial=1,
|
||||||
|
required=False,
|
||||||
|
help_text='Position of the first member device. Increases by one for each additional member.'
|
||||||
|
)
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -4123,12 +4154,47 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = [
|
fields = [
|
||||||
'master', 'domain', 'tags',
|
'name', 'domain', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
instance = super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Assign VC members
|
||||||
|
if instance.pk:
|
||||||
|
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||||
|
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
|
||||||
|
member.virtual_chassis = instance
|
||||||
|
member.vc_position = i
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
master = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = [
|
||||||
|
'name', 'domain', 'master', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'master': SelectWithPK(),
|
'master': SelectWithPK(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
|
||||||
|
|
||||||
|
|
||||||
class BaseVCMemberFormSet(forms.BaseModelFormSet):
|
class BaseVCMemberFormSet(forms.BaseModelFormSet):
|
||||||
|
|
||||||
@ -4221,7 +4287,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
|||||||
device = self.cleaned_data['device']
|
device = self.cleaned_data['device']
|
||||||
if device.virtual_chassis is not None:
|
if device.virtual_chassis is not None:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Device {} is already assigned to a virtual chassis.".format(device)
|
f"Device {device} is already assigned to a virtual chassis."
|
||||||
)
|
)
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def copy_master_name(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy the master device's name to the VirtualChassis.
|
||||||
|
"""
|
||||||
|
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
|
||||||
|
|
||||||
|
for vc in VirtualChassis.objects.prefetch_related('master'):
|
||||||
|
name = vc.master.name if vc.master.name else f'Unnamed VC #{vc.pk}'
|
||||||
|
VirtualChassis.objects.filter(pk=vc.pk).update(name=name)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0109_interface_remove_vm'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='virtualchassis',
|
||||||
|
options={'ordering': ['name'], 'verbose_name_plural': 'virtual chassis'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='master',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_master_name,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=64),
|
||||||
|
),
|
||||||
|
]
|
@ -1572,9 +1572,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
|
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
|
||||||
})
|
})
|
||||||
if self.primary_ip4.interface in vc_interfaces:
|
if self.primary_ip4.assigned_object in vc_interfaces:
|
||||||
pass
|
pass
|
||||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.assigned_object in vc_interfaces:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -1585,9 +1585,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
|
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
|
||||||
})
|
})
|
||||||
if self.primary_ip6.interface in vc_interfaces:
|
if self.primary_ip6.assigned_object in vc_interfaces:
|
||||||
pass
|
pass
|
||||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.assigned_object in vc_interfaces:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -1757,7 +1757,12 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
master = models.OneToOneField(
|
master = models.OneToOneField(
|
||||||
to='Device',
|
to='Device',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='vc_master_for'
|
related_name='vc_master_for',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=64
|
||||||
)
|
)
|
||||||
domain = models.CharField(
|
domain = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
@ -1767,14 +1772,14 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = ['master', 'domain']
|
csv_headers = ['name', 'domain', 'master']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['master']
|
ordering = ['name']
|
||||||
verbose_name_plural = 'virtual chassis'
|
verbose_name_plural = 'virtual chassis'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||||
@ -1783,9 +1788,9 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
|
|
||||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||||
# VirtualChassis.)
|
# VirtualChassis.)
|
||||||
if self.pk and self.master not in self.members.all():
|
if self.pk and self.master and self.master not in self.members.all():
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'master': "The selected master is not assigned to this virtual chassis."
|
'master': f"The selected master ({self.master}) is not assigned to this virtual chassis."
|
||||||
})
|
})
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
@ -1799,8 +1804,7 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
if interfaces:
|
if interfaces:
|
||||||
raise ProtectedError(
|
raise ProtectedError(
|
||||||
"Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis "
|
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
|
||||||
"LAG".format(self),
|
|
||||||
interfaces
|
interfaces
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1808,8 +1812,9 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.master,
|
self.name,
|
||||||
self.domain,
|
self.domain,
|
||||||
|
self.master.name if self.master else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,14 +10,12 @@ from .models import Cable, Device, VirtualChassis
|
|||||||
@receiver(post_save, sender=VirtualChassis)
|
@receiver(post_save, sender=VirtualChassis)
|
||||||
def assign_virtualchassis_master(instance, created, **kwargs):
|
def assign_virtualchassis_master(instance, created, **kwargs):
|
||||||
"""
|
"""
|
||||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
When a VirtualChassis is created, automatically assign its master device (if any) to the VC.
|
||||||
"""
|
"""
|
||||||
if created:
|
if created and instance.master:
|
||||||
devices = Device.objects.filter(pk=instance.master.pk)
|
instance.master.virtual_chassis = instance
|
||||||
for device in devices:
|
instance.master.vc_position = 1
|
||||||
device.virtual_chassis = instance
|
instance.master.save()
|
||||||
device.vc_position = None
|
|
||||||
device.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=VirtualChassis)
|
@receiver(pre_delete, sender=VirtualChassis)
|
||||||
|
@ -1167,7 +1167,9 @@ class InventoryItemTable(BaseTable):
|
|||||||
class VirtualChassisTable(BaseTable):
|
class VirtualChassisTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
accessor=Accessor('master__name'),
|
linkify=True
|
||||||
|
)
|
||||||
|
master = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
member_count = tables.Column(
|
member_count = tables.Column(
|
||||||
@ -1179,8 +1181,8 @@ class VirtualChassisTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ('pk', 'name', 'domain', 'member_count', 'tags')
|
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||||
default_columns = ('pk', 'name', 'domain', 'member_count')
|
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -2003,7 +2003,7 @@ class ConnectedDeviceTest(APITestCase):
|
|||||||
|
|
||||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
brief_fields = ['id', 'master', 'member_count', 'url']
|
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -2040,9 +2040,9 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
# Create three VirtualChassis with three members each
|
# Create three VirtualChassis with three members each
|
||||||
virtual_chassis = (
|
virtual_chassis = (
|
||||||
VirtualChassis(master=devices[0], domain='domain-1'),
|
VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
|
||||||
VirtualChassis(master=devices[3], domain='domain-2'),
|
VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
|
||||||
VirtualChassis(master=devices[6], domain='domain-3'),
|
VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
|
||||||
)
|
)
|
||||||
VirtualChassis.objects.bulk_create(virtual_chassis)
|
VirtualChassis.objects.bulk_create(virtual_chassis)
|
||||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
||||||
@ -2053,21 +2053,22 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
|||||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
||||||
|
|
||||||
cls.update_data = {
|
cls.update_data = {
|
||||||
'master': devices[1].pk,
|
'name': 'Virtual Chassis X',
|
||||||
'domain': 'domain-x',
|
'domain': 'domain-x',
|
||||||
|
'master': devices[1].pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'master': devices[9].pk,
|
'name': 'Virtual Chassis 4',
|
||||||
'domain': 'domain-4',
|
'domain': 'domain-4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'master': devices[10].pk,
|
'name': 'Virtual Chassis 5',
|
||||||
'domain': 'domain-5',
|
'domain': 'domain-5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'master': devices[11].pk,
|
'name': 'Virtual Chassis 6',
|
||||||
'domain': 'domain-6',
|
'domain': 'domain-6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -1563,16 +1563,7 @@ class CableTestCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
# Blocked by standard creation, bulk creation views for VirtualChassis (member devices must be selected in bulk)
|
|
||||||
class VirtualChassisTestCase(
|
|
||||||
ViewTestCases.GetObjectViewTestCase,
|
|
||||||
ViewTestCases.EditObjectViewTestCase,
|
|
||||||
ViewTestCases.DeleteObjectViewTestCase,
|
|
||||||
ViewTestCases.ListObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
|
||||||
):
|
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1602,19 +1593,22 @@ class VirtualChassisTestCase(
|
|||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
# Create three VirtualChassis with two members each
|
# Create three VirtualChassis with two members each
|
||||||
vc1 = VirtualChassis.objects.create(master=devices[0], domain='domain-1')
|
vc1 = VirtualChassis.objects.create(name='VC1', master=devices[0], domain='domain-1')
|
||||||
|
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=vc1, vc_position=1)
|
||||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=vc1, vc_position=2)
|
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=vc1, vc_position=2)
|
||||||
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3)
|
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3)
|
||||||
vc2 = VirtualChassis.objects.create(master=devices[3], domain='domain-2')
|
vc2 = VirtualChassis.objects.create(name='VC2', master=devices[3], domain='domain-2')
|
||||||
|
Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=vc2, vc_position=1)
|
||||||
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=vc2, vc_position=2)
|
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=vc2, vc_position=2)
|
||||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3)
|
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3)
|
||||||
vc3 = VirtualChassis.objects.create(master=devices[6], domain='domain-3')
|
vc3 = VirtualChassis.objects.create(name='VC3', master=devices[6], domain='domain-3')
|
||||||
|
Device.objects.filter(pk=devices[6].pk).update(virtual_chassis=vc3, vc_position=1)
|
||||||
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=vc3, vc_position=2)
|
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=vc3, vc_position=2)
|
||||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)
|
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'master': devices[1].pk,
|
'name': 'VC4',
|
||||||
'domain': 'domain-x',
|
'domain': 'domain-4',
|
||||||
# Management form data for VC members
|
# Management form data for VC members
|
||||||
'form-TOTAL_FORMS': 0,
|
'form-TOTAL_FORMS': 0,
|
||||||
'form-INITIAL_FORMS': 3,
|
'form-INITIAL_FORMS': 3,
|
||||||
|
@ -2117,62 +2117,11 @@ class VirtualChassisView(ObjectView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisCreateView(ObjectPermissionRequiredMixin, View):
|
class VirtualChassisCreateView(ObjectEditView):
|
||||||
queryset = VirtualChassis.objects.all()
|
queryset = VirtualChassis.objects.all()
|
||||||
|
model_form = forms.VirtualChassisCreateForm
|
||||||
def get_required_permission(self):
|
template_name = 'dcim/virtualchassis_add.html'
|
||||||
return 'dcim.add_virtualchassis'
|
default_return_url = 'dcim:virtualchassis_list'
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
|
|
||||||
# Get the list of devices being added to a VirtualChassis
|
|
||||||
pk_form = forms.DeviceSelectionForm(request.POST)
|
|
||||||
pk_form.full_clean()
|
|
||||||
if not pk_form.cleaned_data.get('pk'):
|
|
||||||
messages.warning(request, "No devices were selected.")
|
|
||||||
return redirect('dcim:device_list')
|
|
||||||
device_queryset = Device.objects.filter(
|
|
||||||
pk__in=pk_form.cleaned_data.get('pk')
|
|
||||||
).prefetch_related('rack').order_by('vc_position')
|
|
||||||
|
|
||||||
VCMemberFormSet = modelformset_factory(
|
|
||||||
model=Device,
|
|
||||||
formset=forms.BaseVCMemberFormSet,
|
|
||||||
form=forms.DeviceVCMembershipForm,
|
|
||||||
extra=0
|
|
||||||
)
|
|
||||||
|
|
||||||
if '_create' in request.POST:
|
|
||||||
|
|
||||||
vc_form = forms.VirtualChassisForm(request.POST)
|
|
||||||
vc_form.fields['master'].queryset = device_queryset
|
|
||||||
formset = VCMemberFormSet(request.POST, queryset=device_queryset)
|
|
||||||
|
|
||||||
if vc_form.is_valid() and formset.is_valid():
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
|
|
||||||
# Assign each device to the VirtualChassis before saving
|
|
||||||
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_queryset
|
|
||||||
formset = VCMemberFormSet(queryset=device_queryset)
|
|
||||||
|
|
||||||
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(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
|
class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
|
||||||
@ -2234,7 +2183,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
|
|||||||
for member in members:
|
for member in members:
|
||||||
member.save()
|
member.save()
|
||||||
|
|
||||||
return redirect(vc_form.cleaned_data['master'].get_absolute_url())
|
return redirect(virtual_chassis.get_absolute_url())
|
||||||
|
|
||||||
return render(request, 'dcim/virtualchassis_edit.html', {
|
return render(request, 'dcim/virtualchassis_edit.html', {
|
||||||
'vc_form': vc_form,
|
'vc_form': vc_form,
|
||||||
|
@ -17,9 +17,4 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_virtualchassis %}
|
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
|
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li>
|
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li>
|
||||||
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</a></li>
|
{% if virtualchassis.master %}
|
||||||
|
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</a></li>
|
||||||
|
{% endif %}
|
||||||
<li>{{ virtualchassis }}</li>
|
<li>{{ virtualchassis }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +65,17 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Domain</td>
|
<td>Domain</td>
|
||||||
<td>{{ virtualchassis.domain|placeholder }}</td>
|
<td>{{ virtualchassis.domain|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Master</td>
|
||||||
|
<td>
|
||||||
|
{% if virtualchassis.master %}
|
||||||
|
<a href="{{ virtualchassis.master.get_absolute_url }}">{{ virtualchassis.master }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
|
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
|
||||||
|
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Virtual Chassis</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.domain %}
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Member Devices</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.site %}
|
||||||
|
{% render_field form.rack %}
|
||||||
|
{% render_field form.members %}
|
||||||
|
{% render_field form.initial_position %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -144,6 +144,11 @@
|
|||||||
<a href="{% url 'dcim:platform_list' %}">Platforms</a>
|
<a href="{% url 'dcim:platform_list' %}">Platforms</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
|
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
|
||||||
|
{% if perms.dcim.add_virtualchassis %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'dcim:virtualchassis_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
|
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
Loading…
Reference in New Issue
Block a user