mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on #2018: Add name to VirtualChassis
This commit is contained in:
@ -332,7 +332,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
master = NestedDeviceSerializer()
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@ -4123,12 +4154,47 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
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 = {
|
||||
'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):
|
||||
|
||||
@ -4221,7 +4287,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
||||
device = self.cleaned_data['device']
|
||||
if device.virtual_chassis is not None:
|
||||
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
|
||||
|
||||
|
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({
|
||||
'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
|
||||
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
|
||||
else:
|
||||
raise ValidationError({
|
||||
@ -1585,9 +1585,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
raise ValidationError({
|
||||
'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
|
||||
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
|
||||
else:
|
||||
raise ValidationError({
|
||||
@ -1757,7 +1757,12 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
master = models.OneToOneField(
|
||||
to='Device',
|
||||
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(
|
||||
max_length=30,
|
||||
@ -1767,14 +1772,14 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['master', 'domain']
|
||||
csv_headers = ['name', 'domain', 'master']
|
||||
|
||||
class Meta:
|
||||
ordering = ['master']
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
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
|
||||
# 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({
|
||||
'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):
|
||||
@ -1799,8 +1804,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
)
|
||||
if interfaces:
|
||||
raise ProtectedError(
|
||||
"Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis "
|
||||
"LAG".format(self),
|
||||
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
|
||||
interfaces
|
||||
)
|
||||
|
||||
@ -1808,8 +1812,9 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.master,
|
||||
self.name,
|
||||
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)
|
||||
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:
|
||||
devices = Device.objects.filter(pk=instance.master.pk)
|
||||
for device in devices:
|
||||
device.virtual_chassis = instance
|
||||
device.vc_position = None
|
||||
device.save()
|
||||
if created and instance.master:
|
||||
instance.master.virtual_chassis = instance
|
||||
instance.master.vc_position = 1
|
||||
instance.master.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VirtualChassis)
|
||||
|
@ -1167,7 +1167,9 @@ class InventoryItemTable(BaseTable):
|
||||
class VirtualChassisTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
accessor=Accessor('master__name'),
|
||||
linkify=True
|
||||
)
|
||||
master = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
member_count = tables.Column(
|
||||
@ -1179,8 +1181,8 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'name', 'domain', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'member_count')
|
||||
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
|
||||
#
|
||||
|
@ -2003,7 +2003,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualChassis
|
||||
brief_fields = ['id', 'master', 'member_count', 'url']
|
||||
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -2040,9 +2040,9 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
# Create three VirtualChassis with three members each
|
||||
virtual_chassis = (
|
||||
VirtualChassis(master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(master=devices[6], domain='domain-3'),
|
||||
VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
|
||||
)
|
||||
VirtualChassis.objects.bulk_create(virtual_chassis)
|
||||
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)
|
||||
|
||||
cls.update_data = {
|
||||
'master': devices[1].pk,
|
||||
'name': 'Virtual Chassis X',
|
||||
'domain': 'domain-x',
|
||||
'master': devices[1].pk,
|
||||
}
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'master': devices[9].pk,
|
||||
'name': 'Virtual Chassis 4',
|
||||
'domain': 'domain-4',
|
||||
},
|
||||
{
|
||||
'master': devices[10].pk,
|
||||
'name': 'Virtual Chassis 5',
|
||||
'domain': 'domain-5',
|
||||
},
|
||||
{
|
||||
'master': devices[11].pk,
|
||||
'name': 'Virtual Chassis 6',
|
||||
'domain': 'domain-6',
|
||||
},
|
||||
]
|
||||
|
@ -1563,16 +1563,7 @@ class CableTestCase(
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to 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
|
||||
):
|
||||
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualChassis
|
||||
|
||||
@classmethod
|
||||
@ -1602,19 +1593,22 @@ class VirtualChassisTestCase(
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
# 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[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[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[8].pk).update(virtual_chassis=vc3, vc_position=3)
|
||||
|
||||
cls.form_data = {
|
||||
'master': devices[1].pk,
|
||||
'domain': 'domain-x',
|
||||
'name': 'VC4',
|
||||
'domain': 'domain-4',
|
||||
# Management form data for VC members
|
||||
'form-TOTAL_FORMS': 0,
|
||||
'form-INITIAL_FORMS': 3,
|
||||
|
@ -2117,62 +2117,11 @@ class VirtualChassisView(ObjectView):
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisCreateView(ObjectPermissionRequiredMixin, View):
|
||||
class VirtualChassisCreateView(ObjectEditView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
|
||||
def get_required_permission(self):
|
||||
return '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()
|
||||
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'),
|
||||
})
|
||||
model_form = forms.VirtualChassisCreateForm
|
||||
template_name = 'dcim/virtualchassis_add.html'
|
||||
default_return_url = 'dcim:virtualchassis_list'
|
||||
|
||||
|
||||
class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
|
||||
@ -2234,7 +2183,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
|
||||
for member in members:
|
||||
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', {
|
||||
'vc_form': vc_form,
|
||||
|
@ -17,9 +17,4 @@
|
||||
</ul>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
@ -9,7 +9,9 @@
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</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>
|
||||
</ol>
|
||||
</div>
|
||||
@ -63,7 +65,17 @@
|
||||
<tr>
|
||||
<td>Domain</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>
|
||||
</div>
|
||||
{% 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>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
|
Reference in New Issue
Block a user