mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
57ddd5086f
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
*.pyc
|
||||
configuration.py
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/static
|
||||
.idea
|
||||
/*.sh
|
||||
!upgrade.sh
|
||||
|
@ -98,4 +98,4 @@ dist-switch\d
|
||||
access-switch\d+,oob-switch\d+
|
||||
```
|
||||
|
||||
Note that you can combine multiple regexes onto one line using commas. (Commas can only be used for separating regexes; they will not be processed as part of a regex.) The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
|
@ -18,7 +18,7 @@ Download and extract the latest version:
|
||||
Copy the 'configuration.py' you created when first installing to the new version:
|
||||
|
||||
```
|
||||
# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py
|
||||
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
|
||||
```
|
||||
|
||||
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||
|
@ -79,7 +79,7 @@ class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments', 'custom_fields']
|
||||
'u_height', 'desc_units', 'comments', 'custom_fields']
|
||||
|
||||
|
||||
class RackNestedSerializer(RackSerializer):
|
||||
@ -94,7 +94,7 @@ class RackDetailSerializer(RackSerializer):
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
|
||||
def get_front_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||
|
@ -142,7 +142,8 @@ class RackForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
'comments']
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
@ -178,7 +179,8 @@ class RackFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
|
||||
'desc_units']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -368,7 +370,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None,
|
||||
help_text="For multi-U devices, this is the lowest occupied rack unit.",
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
||||
disabled_indicator='device'))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
@ -582,6 +584,18 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
class DeviceBulkAddComponentForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
||||
@ -1012,10 +1026,6 @@ class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
@ -1226,15 +1236,12 @@ class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'interface', 'set_as_primary']
|
||||
help_texts = {
|
||||
'address': 'IPv4 or IPv6 address (with mask)'
|
||||
}
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'description']
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
|
||||
@ -1251,7 +1258,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
||||
|
20
netbox/dcim/migrations/0020_rack_desc_units.py
Normal file
20
netbox/dcim/migrations/0020_rack_desc_units.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-28 15:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0019_new_iface_form_factors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
|
||||
),
|
||||
]
|
31
netbox/dcim/migrations/0021_add_ff_flexstack.py
Normal file
31
netbox/dcim/migrations/0021_add_ff_flexstack.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-31 18:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0020_rack_desc_units'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
@ -107,6 +107,8 @@ IFACE_FF_E3 = 4050
|
||||
# Stacking
|
||||
IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@ -164,6 +166,8 @@ IFACE_FF_CHOICES = [
|
||||
[
|
||||
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@ -375,6 +379,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
help_text='Rail-to-rail width')
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
desc_units = models.BooleanField(default=False, verbose_name='Descending units',
|
||||
help_text='Units are numbered top-to-bottom')
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@ -401,8 +407,11 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
if self.u_height < min_height:
|
||||
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
||||
.format(min_height))
|
||||
raise ValidationError({
|
||||
'u_height': "Rack must be at least {}U tall to house currently installed devices.".format(
|
||||
min_height
|
||||
)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@ -419,7 +428,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
if self.desc_units:
|
||||
return range(1, self.u_height + 1)
|
||||
else:
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@ -438,7 +450,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
|
||||
elevation = OrderedDict()
|
||||
for u in reversed(range(1, self.u_height + 1)):
|
||||
for u in self.units:
|
||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||
|
||||
# Add devices to rack units list
|
||||
@ -476,7 +488,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
|
||||
# Gather all devices which consume U space within the rack
|
||||
devices = self.devices.select_related().filter(position__gte=1).exclude(pk__in=exclude)
|
||||
devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
||||
|
||||
# Initialize the rack unit skeleton
|
||||
units = range(1, self.u_height + 1)
|
||||
@ -506,9 +518,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
Determine the utilization rate of the rack and return it as a percentage.
|
||||
"""
|
||||
if self.u_consumed is None:
|
||||
self.u_consumed = 0
|
||||
u_available = self.u_height - self.u_consumed
|
||||
u_available = len(self.get_available_units())
|
||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||
|
||||
|
||||
@ -596,27 +606,39 @@ class DeviceType(models.Model):
|
||||
u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required,
|
||||
exclude=[d.pk])
|
||||
if d.position not in u_available:
|
||||
raise ValidationError("Device {} in rack {} does not have sufficient space to accommodate a height "
|
||||
"of {}U".format(d, d.rack, self.u_height))
|
||||
raise ValidationError({
|
||||
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
|
||||
"{}U".format(d, d.rack, self.u_height)
|
||||
})
|
||||
|
||||
if not self.is_console_server and self.cs_port_templates.count():
|
||||
raise ValidationError("Must delete all console server port templates associated with this device before "
|
||||
"declassifying it as a console server.")
|
||||
raise ValidationError({
|
||||
'is_console_server': "Must delete all console server port templates associated with this device before "
|
||||
"declassifying it as a console server."
|
||||
})
|
||||
|
||||
if not self.is_pdu and self.power_outlet_templates.count():
|
||||
raise ValidationError("Must delete all power outlet templates associated with this device before "
|
||||
"declassifying it as a PDU.")
|
||||
raise ValidationError({
|
||||
'is_pdu': "Must delete all power outlet templates associated with this device before declassifying it "
|
||||
"as a PDU."
|
||||
})
|
||||
|
||||
if not self.is_network_device and self.interface_templates.filter(mgmt_only=False).count():
|
||||
raise ValidationError("Must delete all non-management-only interface templates associated with this device "
|
||||
"before declassifying it as a network device.")
|
||||
raise ValidationError({
|
||||
'is_network_device': "Must delete all non-management-only interface templates associated with this "
|
||||
"device before declassifying it as a network device."
|
||||
})
|
||||
|
||||
if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count():
|
||||
raise ValidationError("Must delete all device bay templates associated with this device before "
|
||||
"declassifying it as a parent device.")
|
||||
raise ValidationError({
|
||||
'subdevice_role': "Must delete all device bay templates associated with this device before "
|
||||
"declassifying it as a parent device."
|
||||
})
|
||||
|
||||
if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD:
|
||||
raise ValidationError("Child device types must be 0U.")
|
||||
raise ValidationError({
|
||||
'u_height': "Child device types must be 0U."
|
||||
})
|
||||
|
||||
@property
|
||||
def is_parent_device(self):
|
||||
@ -800,7 +822,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||
verbose_name='Position (U)',
|
||||
help_text='Number of the lowest U position occupied by the device')
|
||||
help_text='The lowest-numbered unit occupied by the device')
|
||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
|
||||
@ -824,29 +846,39 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate device type assignment
|
||||
if not hasattr(self, 'device_type'):
|
||||
raise ValidationError("Must specify device type.")
|
||||
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and (self.face is not None or self.position):
|
||||
raise ValidationError("Child device types cannot be assigned a rack face or position.")
|
||||
|
||||
# Validate position/face combination
|
||||
if self.position and self.face is None:
|
||||
raise ValidationError("Must specify rack face with rack position.")
|
||||
raise ValidationError({
|
||||
'face': "Must specify rack face when defining rack position."
|
||||
})
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError("U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
||||
"{} ({}U).".format(self.position, self.device_type, self.device_type.u_height))
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
if self.device_type:
|
||||
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and self.face is not None:
|
||||
raise ValidationError({
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||
"device."
|
||||
})
|
||||
if self.device_type.is_child_device and self.position:
|
||||
raise ValidationError({
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||
"parent device."
|
||||
})
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError({
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
})
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@ -961,6 +993,9 @@ class ConsolePort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@ -1002,6 +1037,9 @@ class ConsoleServerPort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
|
||||
class PowerPort(models.Model):
|
||||
"""
|
||||
@ -1020,6 +1058,9 @@ class PowerPort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@ -1055,6 +1096,9 @@ class PowerOutlet(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
|
||||
class InterfaceManager(models.Manager):
|
||||
|
||||
@ -1091,12 +1135,16 @@ class Interface(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
|
||||
raise ValidationError({'form_factor': "Virtual interfaces cannot be connected to another interface or "
|
||||
"circuit. Disconnect the interface or choose a physical form "
|
||||
"factor."})
|
||||
raise ValidationError({
|
||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||
"interface or choose a physical form factor."
|
||||
})
|
||||
|
||||
@property
|
||||
def is_physical(self):
|
||||
@ -1147,7 +1195,9 @@ class InterfaceConnection(models.Model):
|
||||
|
||||
def clean(self):
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError("Cannot connect an interface to itself")
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
@ -1176,12 +1226,16 @@ class DeviceBay(models.Model):
|
||||
def __unicode__(self):
|
||||
return u'{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
if not self.device.device_type.is_parent_device:
|
||||
raise ValidationError("This type of device ({}) does not support device bays."
|
||||
.format(self.device.device_type))
|
||||
raise ValidationError("This type of device ({}) does not support device bays.".format(
|
||||
self.device.device_type
|
||||
))
|
||||
|
||||
# Cannot install a device into itself, obviously
|
||||
if self.device == self.installed_device:
|
||||
@ -1208,3 +1262,6 @@ class Module(models.Model):
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return reverse('dcim:device_inventory', args=[self.device.pk])
|
||||
|
@ -72,7 +72,7 @@ STATUS_ICON = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
|
||||
@ -148,13 +148,12 @@ class RackTable(BaseTable):
|
||||
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed',
|
||||
'utilization')
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices',
|
||||
'get_utilization')
|
||||
|
||||
|
||||
class RackImportTable(BaseTable):
|
||||
@ -196,10 +195,12 @@ class DeviceTypeTable(BaseTable):
|
||||
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
|
||||
|
||||
#
|
||||
@ -357,7 +358,7 @@ class PowerConnectionTable(BaseTable):
|
||||
args=[Accessor('power_outlet.device.pk')], verbose_name='PDU')
|
||||
power_outlet = tables.Column(verbose_name='Outlet')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
name = tables.Column(verbose_name='Power Port')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
|
@ -49,6 +49,7 @@ class SiteTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
@ -129,6 +130,7 @@ class RackTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
@ -145,6 +147,7 @@ class RackTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
'front_units',
|
||||
|
@ -110,38 +110,38 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.consoleport_delete, name='consoleport_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.consoleserverport_delete, name='consoleserverport_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.powerport_delete, name='powerport_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
||||
|
||||
@ -154,18 +154,18 @@ urlpatterns = [
|
||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.interface_delete, name='interface_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
|
||||
# Modules
|
||||
url(r'^devices/(?P<pk>\d+)/modules/add/$', views.module_add, name='module_add'),
|
||||
url(r'^modules/(?P<pk>\d+)/edit/$', views.module_edit, name='module_edit'),
|
||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.module_delete, name='module_delete'),
|
||||
url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
|
||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),
|
||||
|
||||
]
|
||||
|
@ -1,3 +1,4 @@
|
||||
from copy import deepcopy
|
||||
import re
|
||||
from natsort import natsorted
|
||||
from operator import attrgetter
|
||||
@ -7,8 +8,8 @@ from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.http import urlencode
|
||||
@ -181,8 +182,7 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class RackListView(ObjectListView):
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
|
||||
.annotate(device_count=Count('devices', distinct=True),
|
||||
u_consumed=Coalesce(Sum('devices__device_type__u_height'), 0))
|
||||
.annotate(device_count=Count('devices', distinct=True))
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
table = tables.RackTable
|
||||
@ -275,7 +275,7 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceTypeListView(ObjectListView):
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
filter_form = forms.DeviceTypeFilterForm
|
||||
table = tables.DeviceTypeTable
|
||||
@ -394,7 +394,7 @@ class ComponentTemplateCreateView(View):
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(component_templates)
|
||||
messages.success(request, "Added {} component(s) to {}".format(len(component_templates), devicetype))
|
||||
messages.success(request, u"Added {} component(s) to {}.".format(len(component_templates), devicetype))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
@ -574,7 +574,8 @@ def device(request, pk):
|
||||
secrets = device.secrets.all()
|
||||
|
||||
# Find all IP addresses assigned to this device
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface').order_by('address')
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
||||
.order_by('address')
|
||||
|
||||
# Find any related devices for convenient linking in the UI
|
||||
related_devices = []
|
||||
@ -687,6 +688,80 @@ def device_lldp_neighbors(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddComponentView(View):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = None
|
||||
component_cls = None
|
||||
component_form = None
|
||||
|
||||
def get(self):
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
def post(self, request):
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
if '_create' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
data = deepcopy(form.cleaned_data)
|
||||
for device in data['pk']:
|
||||
|
||||
names = data['name_pattern']
|
||||
for name in names:
|
||||
component_data = {
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.component_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate {} name for {}: {}".format(
|
||||
self.component_cls._meta.verbose_name, device, name
|
||||
))
|
||||
|
||||
if not form.errors:
|
||||
self.component_cls.objects.bulk_create(new_components)
|
||||
messages.success(request, u"Added {} {} to {} devices.".format(
|
||||
len(new_components), self.component_cls._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||
))
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': pk_list})
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
if not selected_devices:
|
||||
messages.warning(request, u"No devices were selected.")
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||
'form': form,
|
||||
'component_name': self.component_cls._meta.verbose_name_plural,
|
||||
'selected_devices': selected_devices,
|
||||
'cancel_url': reverse('dcim:device_list'),
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceView(DeviceBulkAddComponentView):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
component_cls = Interface
|
||||
component_form = forms.InterfaceForm
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
@ -713,7 +788,7 @@ def consoleport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
ConsolePort.objects.bulk_create(console_ports)
|
||||
messages.success(request, "Added {} console port(s) to {}".format(len(console_ports), device))
|
||||
messages.success(request, u"Added {} console port(s) to {}.".format(len(console_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleport_add', pk=device.pk)
|
||||
else:
|
||||
@ -722,8 +797,9 @@ def consoleport_add(request, pk):
|
||||
else:
|
||||
form = forms.ConsolePortCreateForm()
|
||||
|
||||
return render(request, 'dcim/consoleport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@ -738,7 +814,7 @@ def consoleport_connect(request, pk):
|
||||
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleport.cs_port.device,
|
||||
@ -765,7 +841,7 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if not consoleport.cs_port:
|
||||
messages.warning(request, "Cannot disconnect console port {0}: It is not connected to anything"
|
||||
messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything."
|
||||
.format(consoleport))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
@ -775,7 +851,7 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, "Console port {0} has been disconnected".format(consoleport))
|
||||
messages.success(request, u"Console port {} has been disconnected.".format(consoleport))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@ -788,49 +864,15 @@ def consoleport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleport')
|
||||
def consoleport_edit(request, pk):
|
||||
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsolePortForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, "Modified {0} {1}".format(consoleport.device.name, consoleport.name))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsolePortForm(instance=consoleport)
|
||||
|
||||
return render(request, 'dcim/consoleport_edit.html', {
|
||||
'consoleport': consoleport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||
})
|
||||
class ConsolePortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
model = ConsolePort
|
||||
form_class = forms.ConsolePortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_consoleport')
|
||||
def consoleport_delete(request, pk):
|
||||
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
consoleport.delete()
|
||||
messages.success(request, "Console port {0} has been deleted from {1}".format(consoleport,
|
||||
consoleport.device))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/consoleport_delete.html', {
|
||||
'consoleport': consoleport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||
})
|
||||
class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
model = ConsolePort
|
||||
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -873,7 +915,7 @@ def consoleserverport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
ConsoleServerPort.objects.bulk_create(cs_ports)
|
||||
messages.success(request, "Added {} console server port(s) to {}".format(len(cs_ports), device))
|
||||
messages.success(request, u"Added {} console server port(s) to {}.".format(len(cs_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleserverport_add', pk=device.pk)
|
||||
else:
|
||||
@ -882,8 +924,9 @@ def consoleserverport_add(request, pk):
|
||||
else:
|
||||
form = forms.ConsoleServerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/consoleserverport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Server Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@ -901,7 +944,7 @@ def consoleserverport_connect(request, pk):
|
||||
consoleport.cs_port = consoleserverport
|
||||
consoleport.connection_status = form.cleaned_data['connection_status']
|
||||
consoleport.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleserverport.device,
|
||||
@ -925,7 +968,7 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if not hasattr(consoleserverport, 'connected_console'):
|
||||
messages.warning(request, "Cannot disconnect console server port {0}: Nothing is connected to it"
|
||||
messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it."
|
||||
.format(consoleserverport))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
@ -936,7 +979,7 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, "Console server port {0} has been disconnected".format(consoleserverport))
|
||||
messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@ -949,49 +992,15 @@ def consoleserverport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleserverport')
|
||||
def consoleserverport_edit(request, pk):
|
||||
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsoleServerPortForm(request.POST, instance=consoleserverport)
|
||||
if form.is_valid():
|
||||
consoleserverport = form.save()
|
||||
messages.success(request, "Modified {0} {1}".format(consoleserverport.device.name, consoleserverport.name))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsoleServerPortForm(instance=consoleserverport)
|
||||
|
||||
return render(request, 'dcim/consoleserverport_edit.html', {
|
||||
'consoleserverport': consoleserverport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||
})
|
||||
class ConsoleServerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
form_class = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_consoleserverport')
|
||||
def consoleserverport_delete(request, pk):
|
||||
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
consoleserverport.delete()
|
||||
messages.success(request, "Console server port {0} has been deleted from {1}"
|
||||
.format(consoleserverport, consoleserverport.device))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/consoleserverport_delete.html', {
|
||||
'consoleserverport': consoleserverport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||
})
|
||||
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -1026,7 +1035,7 @@ def powerport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
messages.success(request, "Added {} power port(s) to {}".format(len(power_ports), device))
|
||||
messages.success(request, u"Added {} power port(s) to {}.".format(len(power_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:powerport_add', pk=device.pk)
|
||||
else:
|
||||
@ -1035,8 +1044,9 @@ def powerport_add(request, pk):
|
||||
else:
|
||||
form = forms.PowerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/powerport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@ -1051,7 +1061,7 @@ def powerport_connect(request, pk):
|
||||
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
powerport.power_outlet.device,
|
||||
@ -1078,7 +1088,7 @@ def powerport_disconnect(request, pk):
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if not powerport.power_outlet:
|
||||
messages.warning(request, "Cannot disconnect power port {0}: It is not connected to an outlet"
|
||||
messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet."
|
||||
.format(powerport))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
@ -1088,7 +1098,7 @@ def powerport_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, "Power port {0} has been disconnected".format(powerport))
|
||||
messages.success(request, u"Power port {} has been disconnected.".format(powerport))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@ -1101,48 +1111,15 @@ def powerport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_powerport')
|
||||
def powerport_edit(request, pk):
|
||||
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerPortForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, "Modified {0} power port {1}".format(powerport.device.name, powerport.name))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerPortForm(instance=powerport)
|
||||
|
||||
return render(request, 'dcim/powerport_edit.html', {
|
||||
'powerport': powerport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||
})
|
||||
class PowerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
model = PowerPort
|
||||
form_class = forms.PowerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_powerport')
|
||||
def powerport_delete(request, pk):
|
||||
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
powerport.delete()
|
||||
messages.success(request, "Power port {0} has been deleted from {1}".format(powerport, powerport.device))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/powerport_delete.html', {
|
||||
'powerport': powerport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||
})
|
||||
class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
model = PowerPort
|
||||
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -1184,7 +1161,7 @@ def poweroutlet_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
messages.success(request, "Added {} power outlet(s) to {}".format(len(power_outlets), device))
|
||||
messages.success(request, u"Added {} power outlet(s) to {}.".format(len(power_outlets), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:poweroutlet_add', pk=device.pk)
|
||||
else:
|
||||
@ -1193,8 +1170,9 @@ def poweroutlet_add(request, pk):
|
||||
else:
|
||||
form = forms.PowerOutletCreateForm()
|
||||
|
||||
return render(request, 'dcim/poweroutlet_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Outlet',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@ -1212,7 +1190,7 @@ def poweroutlet_connect(request, pk):
|
||||
powerport.power_outlet = poweroutlet
|
||||
powerport.connection_status = form.cleaned_data['connection_status']
|
||||
powerport.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
poweroutlet.device,
|
||||
@ -1236,7 +1214,7 @@ def poweroutlet_disconnect(request, pk):
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if not hasattr(poweroutlet, 'connected_port'):
|
||||
messages.warning(request, "Cannot disconnect power outlet {0}: Nothing is connected to it".format(poweroutlet))
|
||||
messages.warning(request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
@ -1246,7 +1224,7 @@ def poweroutlet_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, "Power outlet {0} has been disconnected".format(poweroutlet))
|
||||
messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@ -1259,49 +1237,15 @@ def poweroutlet_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_poweroutlet')
|
||||
def poweroutlet_edit(request, pk):
|
||||
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerOutletForm(request.POST, instance=poweroutlet)
|
||||
if form.is_valid():
|
||||
poweroutlet = form.save()
|
||||
messages.success(request, "Modified {0} power outlet {1}".format(poweroutlet.device.name, poweroutlet.name))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerOutletForm(instance=poweroutlet)
|
||||
|
||||
return render(request, 'dcim/poweroutlet_edit.html', {
|
||||
'poweroutlet': poweroutlet,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||
})
|
||||
class PowerOutletEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
model = PowerOutlet
|
||||
form_class = forms.PowerOutletForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_poweroutlet')
|
||||
def poweroutlet_delete(request, pk):
|
||||
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
poweroutlet.delete()
|
||||
messages.success(request, "Power outlet {0} has been deleted from {1}".format(poweroutlet,
|
||||
poweroutlet.device))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/poweroutlet_delete.html', {
|
||||
'poweroutlet': poweroutlet,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||
})
|
||||
class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -1340,97 +1284,32 @@ def interface_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
messages.success(request, "Added {} interface(s) to {}".format(len(interfaces), device))
|
||||
messages.success(request, u"Added {} interface(s) to {}.".format(len(interfaces), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:interface_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.InterfaceCreateForm()
|
||||
form = forms.InterfaceCreateForm(initial={'mgmt_only': request.GET.get('mgmt_only')})
|
||||
|
||||
return render(request, 'dcim/interface_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Interface',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_interface')
|
||||
def interface_edit(request, pk):
|
||||
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.InterfaceForm(request.POST, instance=interface)
|
||||
if form.is_valid():
|
||||
interface = form.save()
|
||||
messages.success(request, "Modified {0} interface {1}".format(interface.device.name, interface.name))
|
||||
return redirect('dcim:device', pk=interface.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.InterfaceForm(instance=interface)
|
||||
|
||||
return render(request, 'dcim/interface_edit.html', {
|
||||
'interface': interface,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': interface.device.pk}),
|
||||
})
|
||||
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model = Interface
|
||||
form_class = forms.InterfaceForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_interface')
|
||||
def interface_delete(request, pk):
|
||||
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
interface.delete()
|
||||
messages.success(request, "Interface {0} has been deleted from {1}".format(interface, interface.device))
|
||||
return redirect('dcim:device', pk=interface.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/interface_delete.html', {
|
||||
'interface': interface,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': interface.device.pk}),
|
||||
})
|
||||
|
||||
|
||||
class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
cls = Device
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
template_name = 'dcim/interface_add_multi.html'
|
||||
default_redirect_url = 'dcim:device_list'
|
||||
|
||||
def update_objects(self, pk_list, form, fields):
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
interfaces = []
|
||||
|
||||
for device in selected_devices:
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
iface_form = forms.InterfaceForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
'mac_address': form.cleaned_data['mac_address'],
|
||||
'form_factor': form.cleaned_data['form_factor'],
|
||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
||||
'description': form.cleaned_data['description'],
|
||||
})
|
||||
if iface_form.is_valid():
|
||||
interfaces.append(iface_form.save(commit=False))
|
||||
else:
|
||||
form.add_error(None, "Duplicate interface {} found for device {}".format(name, device))
|
||||
|
||||
if not form.errors:
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
messages.success(self.request, "Added {} interfaces to {} devices".format(len(interfaces),
|
||||
len(selected_devices)))
|
||||
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
model = Interface
|
||||
|
||||
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
@ -1474,7 +1353,7 @@ def devicebay_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
messages.success(request, "Added {} device bay(s) to {}".format(len(device_bays), device))
|
||||
messages.success(request, u"Added {} device bay(s) to {}.".format(len(device_bays), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:devicebay_add', pk=device.pk)
|
||||
else:
|
||||
@ -1483,55 +1362,23 @@ def devicebay_add(request, pk):
|
||||
else:
|
||||
form = forms.DeviceBayCreateForm()
|
||||
|
||||
return render(request, 'dcim/devicebay_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Device Bay',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_devicebay')
|
||||
def devicebay_edit(request, pk):
|
||||
|
||||
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.DeviceBayForm(request.POST, instance=devicebay)
|
||||
if form.is_valid():
|
||||
devicebay = form.save()
|
||||
messages.success(request, "Modified {} bay {}".format(devicebay.device.name, devicebay.name))
|
||||
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.DeviceBayForm(instance=devicebay)
|
||||
|
||||
return render(request, 'dcim/devicebay_edit.html', {
|
||||
'devicebay': devicebay,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||
})
|
||||
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
model = DeviceBay
|
||||
form_class = forms.DeviceBayForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_devicebay')
|
||||
def devicebay_delete(request, pk):
|
||||
|
||||
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
devicebay.delete()
|
||||
messages.success(request, "Device bay {} has been deleted from {}".format(devicebay, devicebay.device))
|
||||
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/devicebay_delete.html', {
|
||||
'devicebay': devicebay,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||
})
|
||||
class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
model = DeviceBay
|
||||
|
||||
|
||||
@permission_required('dcim.change_devicebay')
|
||||
@ -1547,7 +1394,7 @@ def devicebay_populate(request, pk):
|
||||
device_bay.save()
|
||||
|
||||
if not form.errors:
|
||||
messages.success(request, "Added {} to {}".format(device_bay.installed_device, device_bay))
|
||||
messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
|
||||
else:
|
||||
@ -1571,7 +1418,7 @@ def devicebay_depopulate(request, pk):
|
||||
removed_device = device_bay.installed_device
|
||||
device_bay.installed_device = None
|
||||
device_bay.save()
|
||||
messages.success(request, "{} has been removed from {}".format(removed_device, device_bay))
|
||||
messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay))
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
|
||||
else:
|
||||
@ -1603,7 +1450,7 @@ def interfaceconnection_add(request, pk):
|
||||
form = forms.InterfaceConnectionForm(device, request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
@ -1643,7 +1490,7 @@ def interfaceconnection_delete(request, pk):
|
||||
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection.delete()
|
||||
messages.success(request, "Deleted the connection between {0} {1} and {2} {3}".format(
|
||||
messages.success(request, u"Deleted the connection between {} {} and {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
@ -1715,7 +1562,7 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
@permission_required('ipam.add_ipaddress')
|
||||
@permission_required(['dcim.change_device', 'ipam.add_ipaddress'])
|
||||
def ipaddress_assign(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
@ -1727,8 +1574,8 @@ def ipaddress_assign(request, pk):
|
||||
ipaddress = form.save(commit=False)
|
||||
ipaddress.interface = form.cleaned_data['interface']
|
||||
ipaddress.save()
|
||||
messages.success(request, "Added new IP address {0} to interface {1}".format(ipaddress,
|
||||
ipaddress.interface))
|
||||
form.save_custom_fields()
|
||||
messages.success(request, u"Added new IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
|
||||
|
||||
if form.cleaned_data['set_as_primary']:
|
||||
if ipaddress.family == 4:
|
||||
@ -1767,7 +1614,7 @@ def module_add(request, pk):
|
||||
module = form.save(commit=False)
|
||||
module.device = device
|
||||
module.save()
|
||||
messages.success(request, "Added module {} to {}".format(module.name, module.device.name))
|
||||
messages.success(request, u"Added module {} to {}".format(module.name, module.device.name))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:module_add', pk=module.device.pk)
|
||||
else:
|
||||
@ -1776,52 +1623,20 @@ def module_add(request, pk):
|
||||
else:
|
||||
form = forms.ModuleForm()
|
||||
|
||||
return render(request, 'dcim/module_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Module',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_module')
|
||||
def module_edit(request, pk):
|
||||
|
||||
module = get_object_or_404(Module, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ModuleForm(request.POST, instance=module)
|
||||
if form.is_valid():
|
||||
module = form.save()
|
||||
messages.success(request, "Modified {} module {}".format(module.device.name, module.name))
|
||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ModuleForm(instance=module)
|
||||
|
||||
return render(request, 'dcim/module_edit.html', {
|
||||
'module': module,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': module.device.pk}),
|
||||
})
|
||||
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_module'
|
||||
model = Module
|
||||
form_class = forms.ModuleForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_module')
|
||||
def module_delete(request, pk):
|
||||
|
||||
module = get_object_or_404(Module, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
module.delete()
|
||||
messages.success(request, "Module {} has been deleted from {}".format(module, module.device))
|
||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/module_delete.html', {
|
||||
'module': module,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': module.device.pk}),
|
||||
})
|
||||
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_module'
|
||||
model = Module
|
||||
|
@ -80,7 +80,7 @@ class TopologyMapView(APIView):
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.split(','):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query)
|
||||
for d in devices:
|
||||
subgraph.node(d.name)
|
||||
@ -94,7 +94,7 @@ class TopologyMapView(APIView):
|
||||
# Compile list of all devices
|
||||
device_superset = Q()
|
||||
for device_set in tmap.device_sets:
|
||||
for query in device_set.split(','):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
|
||||
# Add all connections to the graph
|
||||
|
@ -142,7 +142,6 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
||||
self.fields[name] = field
|
||||
# Annotate this as a custom field
|
||||
self.custom_fields.append(name)
|
||||
print(self.nullable_fields)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(forms.Form):
|
||||
|
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-11-03 18:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.models import TopologyMap
|
||||
|
||||
|
||||
def commas_to_semicolons(apps, schema_editor):
|
||||
for tm in TopologyMap.objects.filter(device_patterns__contains=','):
|
||||
tm.device_patterns = tm.device_patterns.replace(',', ';')
|
||||
tm.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0003_exporttemplate_add_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='topologymap',
|
||||
name='device_patterns',
|
||||
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||
),
|
||||
migrations.RunPython(commas_to_semicolons),
|
||||
]
|
@ -268,10 +268,11 @@ class TopologyMap(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True)
|
||||
device_patterns = models.TextField(help_text="Identify devices to include in the diagram using regular expressions,"
|
||||
"one per line. Each line will result in a new tier of the drawing. "
|
||||
"Separate multiple regexes on a line using commas. Devices will be "
|
||||
"rendered in the order they are defined.")
|
||||
device_patterns = models.TextField(
|
||||
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
|
||||
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
|
||||
"Devices will be rendered in the order they are defined."
|
||||
)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -159,8 +159,8 @@ class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside',
|
||||
'custom_fields']
|
||||
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside',
|
||||
'nat_outside', 'custom_fields']
|
||||
|
||||
|
||||
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||
|
@ -232,7 +232,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['q', 'family', 'device_id', 'device', 'interface_id']
|
||||
fields = ['q', 'family', 'status', 'device_id', 'device', 'interface_id']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
|
@ -1,20 +1,19 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
|
||||
VLAN_STATUS_CHOICES, VRF,
|
||||
)
|
||||
|
||||
|
||||
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
||||
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||
IP_FAMILY_CHOICES = [
|
||||
('', 'All'),
|
||||
(4, 'IPv4'),
|
||||
@ -173,16 +172,6 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
else:
|
||||
self.fields['vlan'].choices = []
|
||||
|
||||
def clean_prefix(self):
|
||||
prefix = self.cleaned_data['prefix']
|
||||
if prefix.version == 4 and prefix.prefixlen == 32:
|
||||
raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
|
||||
"addresses instead.")
|
||||
elif prefix.version == 6 and prefix.prefixlen == 128:
|
||||
raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
|
||||
"addresses instead.")
|
||||
return prefix
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
@ -248,7 +237,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
@ -295,16 +284,12 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
||||
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
||||
)
|
||||
nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)',
|
||||
widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
|
||||
display_field='address'))
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
|
||||
help_texts = {
|
||||
'address': "IPv4 or IPv6 address and mask",
|
||||
'vrf': "VRF (if applicable)",
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description']
|
||||
widgets = {
|
||||
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -347,11 +332,35 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', display_field='display_name', attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/'))
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(IPAddressAssignForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].choices = []
|
||||
self.fields['device'].choices = []
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
interface_name = forms.CharField(required=False)
|
||||
@ -359,7 +368,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
|
||||
fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -406,12 +415,20 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['vrf', 'tenant', 'description']
|
||||
|
||||
|
||||
def ipaddress_status_choices():
|
||||
status_counts = {}
|
||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||
|
||||
|
||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = IPAddress
|
||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||
@ -422,6 +439,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||
|
||||
|
||||
#
|
||||
@ -479,7 +497,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
@ -510,7 +528,7 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(VLAN_STATUS_CHOICES), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
20
netbox/ipam/migrations/0009_ipaddress_add_status.py
Normal file
20
netbox/ipam/migrations/0009_ipaddress_add_status.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-21 15:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0008_prefix_change_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (5, b'DHCP')], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
]
|
27
netbox/ipam/migrations/0010_ipaddress_help_texts.py
Normal file
27
netbox/ipam/migrations/0010_ipaddress_help_texts.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-11-01 17:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0009_ipaddress_add_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='address',
|
||||
field=ipam.fields.IPAddressField(help_text=b'IPv4 or IPv6 address (with mask)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='nat_inside',
|
||||
field=models.OneToOneField(blank=True, help_text=b'The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name=b'NAT (Inside)'),
|
||||
),
|
||||
]
|
@ -29,6 +29,12 @@ PREFIX_STATUS_CHOICES = (
|
||||
(3, 'Deprecated')
|
||||
)
|
||||
|
||||
IPADDRESS_STATUS_CHOICES = (
|
||||
(1, 'Active'),
|
||||
(2, 'Reserved'),
|
||||
(5, 'DHCP')
|
||||
)
|
||||
|
||||
VLAN_STATUS_CHOICES = (
|
||||
(1, 'Active'),
|
||||
(2, 'Reserved'),
|
||||
@ -40,6 +46,8 @@ STATUS_CHOICE_CLASSES = {
|
||||
1: 'primary',
|
||||
2: 'info',
|
||||
3: 'danger',
|
||||
4: 'warning',
|
||||
5: 'success',
|
||||
}
|
||||
|
||||
|
||||
@ -131,16 +139,22 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
if self.pk:
|
||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||
if covering_aggregates:
|
||||
raise ValidationError("{} is already covered by an existing aggregate ({})"
|
||||
.format(self.prefix, covering_aggregates[0]))
|
||||
raise ValidationError({
|
||||
'prefix': "Aggregates cannot overlap. {} is already covered by an existing aggregate ({}).".format(
|
||||
self.prefix, covering_aggregates[0]
|
||||
)
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
raise ValidationError("{} overlaps with an existing aggregate ({})"
|
||||
.format(self.prefix, covered_aggregates[0]))
|
||||
raise ValidationError({
|
||||
'prefix': "Aggregates cannot overlap. {} covers an existing aggregate ({}).".format(
|
||||
self.prefix, covered_aggregates[0]
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
@ -260,14 +274,17 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Disallow host masks
|
||||
if self.prefix:
|
||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
|
||||
"instead.")
|
||||
raise ValidationError({
|
||||
'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead."
|
||||
})
|
||||
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
|
||||
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
|
||||
"instead.")
|
||||
raise ValidationError({
|
||||
'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
@ -329,14 +346,16 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
||||
address = IPAddressField()
|
||||
address = IPAddressField(help_text="IPv4 or IPv6 address (with mask)")
|
||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||
verbose_name='VRF')
|
||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
||||
status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1)
|
||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
||||
null=True)
|
||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
||||
null=True, verbose_name='NAT IP (inside)')
|
||||
null=True, verbose_name='NAT (Inside)',
|
||||
help_text="The IP for which this address is the \"outside\" IP")
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@ -360,13 +379,16 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
|
||||
.exclude(pk=self.pk)
|
||||
if duplicate_ips:
|
||||
raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf,
|
||||
duplicate_ips.first()))
|
||||
raise ValidationError({
|
||||
'address': "Duplicate IP address found in VRF {}: {}".format(self.vrf, duplicate_ips.first())
|
||||
})
|
||||
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
|
||||
.exclude(pk=self.pk)
|
||||
if duplicate_ips:
|
||||
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
|
||||
raise ValidationError({
|
||||
'address': "Duplicate IP address found in global table: {}".format(duplicate_ips.first())
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.address:
|
||||
@ -387,6 +409,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
str(self.address),
|
||||
self.vrf.rd if self.vrf else '',
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.get_status_display(),
|
||||
self.device.identifier if self.device else '',
|
||||
self.interface.name if self.interface else '',
|
||||
'True' if is_primary else '',
|
||||
@ -399,6 +422,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class VLANGroup(models.Model):
|
||||
"""
|
||||
@ -465,7 +491,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
# Validate VLAN group
|
||||
if self.group and self.group.site != self.site:
|
||||
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
|
||||
raise ValidationError({
|
||||
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
|
@ -14,7 +14,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@ -125,13 +125,13 @@ class AggregateTable(BaseTable):
|
||||
prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate')
|
||||
rir = tables.Column(verbose_name='RIR')
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
description = tables.Column(orderable=False, verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
@ -193,6 +193,7 @@ class PrefixBriefTable(BaseTable):
|
||||
class IPAddressTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||
@ -202,7 +203,7 @@ class IPAddressTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
|
@ -56,6 +56,8 @@ urlpatterns = [
|
||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/remove/$', views.ipaddress_remove, name='ipaddress_remove'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
|
||||
# VLAN groups
|
||||
|
@ -1,11 +1,15 @@
|
||||
import netaddr
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
@ -446,6 +450,73 @@ def ipaddress(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
|
||||
def ipaddress_assign(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.IPAddressAssignForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
interface = form.cleaned_data['interface']
|
||||
ipaddress.interface = interface
|
||||
ipaddress.save()
|
||||
messages.success(request, u"Assigned IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
|
||||
|
||||
if form.cleaned_data['set_as_primary']:
|
||||
device = interface.device
|
||||
if ipaddress.family == 4:
|
||||
device.primary_ip4 = ipaddress
|
||||
elif ipaddress.family == 6:
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = forms.IPAddressAssignForm()
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
|
||||
def ipaddress_remove(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
device = ipaddress.interface.device
|
||||
ipaddress.interface = None
|
||||
ipaddress.save()
|
||||
messages.success(request, u"Removed IP address {} from {}.".format(ipaddress, device))
|
||||
|
||||
if device.primary_ip4 == ipaddress.pk:
|
||||
device.primary_ip4 = None
|
||||
device.save()
|
||||
elif device.primary_ip6 == ipaddress.pk:
|
||||
device.primary_ip6 = None
|
||||
device.save()
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/ipaddress_unassign.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||
})
|
||||
|
||||
|
||||
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
model = IPAddress
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.6.3'
|
||||
VERSION = '1.7.0'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
@ -1,9 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.views.defaults import page_not_found
|
||||
|
||||
from views import home, trigger_500, handle_500
|
||||
from views import home, handle_500, trigger_500
|
||||
from users.views import login, logout
|
||||
|
||||
|
||||
@ -36,7 +35,6 @@ _patterns = [
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
# Error testing
|
||||
url(r'^404/$', page_not_found),
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
# Admin
|
||||
|
@ -47,16 +47,20 @@ def home(request):
|
||||
})
|
||||
|
||||
|
||||
def trigger_500(request):
|
||||
"""Hot-wired method of triggering a server error to test reporting."""
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
||||
|
||||
|
||||
def handle_500(request):
|
||||
"""Custom server error handler"""
|
||||
"""
|
||||
Custom server error handler
|
||||
"""
|
||||
type_, error, traceback = sys.exc_info()
|
||||
return render(request, '500.html', {
|
||||
'exception': str(type_),
|
||||
'error': error,
|
||||
}, status=500)
|
||||
|
||||
|
||||
def trigger_500(request):
|
||||
"""
|
||||
Hot-wired method of triggering a server error to test reporting
|
||||
"""
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
||||
|
@ -34,7 +34,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
try:
|
||||
my_userkey = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.error(request, "You do not have an active User Key.")
|
||||
messages.error(request, u"You do not have an active User Key.")
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
|
||||
if 'activate' in request.POST:
|
||||
@ -46,7 +46,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
uk.activate(master_key)
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
except ValueError:
|
||||
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
||||
messages.error(request, u"Invalid private key provided. Unable to retrieve master key.")
|
||||
else:
|
||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||
|
||||
|
@ -14,10 +14,10 @@ def userkey_required():
|
||||
try:
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.warning(request, "This operation requires an active user key, but you don't have one.")
|
||||
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
|
||||
return redirect('users:userkey')
|
||||
if not uk.is_active():
|
||||
messages.warning(request, "This operation is not available. Your user key has not been activated.")
|
||||
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
|
||||
return redirect('users:userkey')
|
||||
return view(request, *args, **kwargs)
|
||||
return wrapped_view
|
||||
|
@ -49,22 +49,23 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class SecretForm(forms.ModelForm, BootstrapMixin):
|
||||
private_key = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||
widget=forms.TextInput(attrs={'class': 'requires-private-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)')
|
||||
widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)',
|
||||
widget=forms.PasswordInput())
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['role', 'name', 'plaintext', 'plaintext2']
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.cleaned_data['plaintext']:
|
||||
validate_rsa_key(self.cleaned_data['private_key'])
|
||||
|
||||
def clean_plaintext2(self):
|
||||
plaintext = self.cleaned_data['plaintext']
|
||||
plaintext2 = self.cleaned_data['plaintext2']
|
||||
if plaintext != plaintext2:
|
||||
raise forms.ValidationError("The two given plaintext values do not match. Please check your input.")
|
||||
if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
|
||||
raise forms.ValidationError({
|
||||
'plaintext2': "The two given plaintext values do not match. Please check your input."
|
||||
})
|
||||
|
||||
|
||||
class SecretFromCSVForm(forms.ModelForm):
|
||||
|
@ -81,24 +81,34 @@ class UserKey(CreatedUpdatedModel):
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
|
||||
# Validate the public key format and length.
|
||||
if self.public_key:
|
||||
|
||||
# Validate the public key format
|
||||
try:
|
||||
pubkey = RSA.importKey(self.public_key)
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid RSA key format.")
|
||||
raise ValidationError({
|
||||
'public_key': "Invalid RSA key format."
|
||||
})
|
||||
except:
|
||||
raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're "
|
||||
"uploading a valid RSA public key in PEM format (no SSH/PGP).")
|
||||
# key.size() returns 1 less than the key modulus
|
||||
pubkey_length = pubkey.size() + 1
|
||||
|
||||
# Validate the public key length
|
||||
pubkey_length = pubkey.size() + 1 # key.size() returns 1 less than the key modulus
|
||||
if pubkey_length < settings.SECRETS_MIN_PUBKEY_SIZE:
|
||||
raise ValidationError("Insufficient key length. Keys must be at least {} bits long."
|
||||
.format(settings.SECRETS_MIN_PUBKEY_SIZE))
|
||||
raise ValidationError({
|
||||
'public_key': "Insufficient key length. Keys must be at least {} bits long.".format(
|
||||
settings.SECRETS_MIN_PUBKEY_SIZE
|
||||
)
|
||||
})
|
||||
# We can't use keys bigger than our master_key_cipher field can hold
|
||||
if pubkey_length > 4096:
|
||||
raise ValidationError("Public key size ({}) is too large. Maximum key size is 4096 bits."
|
||||
.format(pubkey_length))
|
||||
raise ValidationError({
|
||||
'public_key': "Public key size ({}) is too large. Maximum key size is 4096 bits.".format(
|
||||
pubkey_length
|
||||
)
|
||||
})
|
||||
|
||||
super(UserKey, self).clean()
|
||||
|
||||
|
@ -90,7 +90,7 @@ def secret_add(request, pk):
|
||||
secret.encrypt(master_key)
|
||||
secret.save()
|
||||
|
||||
messages.success(request, "Added new secret: {0}".format(secret))
|
||||
messages.success(request, u"Added new secret: {}.".format(secret))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||
else:
|
||||
@ -135,7 +135,7 @@ def secret_edit(request, pk):
|
||||
else:
|
||||
secret = form.save()
|
||||
|
||||
messages.success(request, "Modified secret {0}".format(secret))
|
||||
messages.success(request, u"Modified secret {}.".format(secret))
|
||||
return redirect('secrets:secret', pk=secret.pk)
|
||||
|
||||
else:
|
||||
@ -180,7 +180,7 @@ def secret_import(request):
|
||||
new_secrets.append(secret)
|
||||
|
||||
table = tables.SecretTable(new_secrets)
|
||||
messages.success(request, "Imported {} new secrets".format(len(new_secrets)))
|
||||
messages.success(request, u"Imported {} new secrets.".format(len(new_secrets)))
|
||||
|
||||
return render(request, 'import_success.html', {
|
||||
'table': table,
|
||||
|
19
netbox/templates/404.html
Normal file
19
netbox/templates/404.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="glyphicon glyphicon-warning-sign"></i> Page Not Found</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
The requested page does not exist.
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'home' %}" class="btn btn-xs btn-primary">Home Page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -131,6 +131,21 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Addressing</td>
|
||||
<td>
|
||||
{% if circuit.interface %}
|
||||
{% for ip in circuit.interface.ip_addresses.all %}
|
||||
{% if not forloop.first %}<br />{% endif %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
||||
{% empty %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</td>
|
||||
<td>
|
||||
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if consoleport.pk %}Editing {{ consoleport.device }} {{ consoleport }}{% else %}Add a Console Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if consoleport.pk %}
|
||||
<strong>Editing {{ consoleport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Console Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if consoleport %}{{ consoleport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if consoleport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if consoleserverport.pk %}Editing {{ consoleserverport.device }} {{ consoleserverport }}{% else %}Add a Console Server Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if consoleserverport.pk %}
|
||||
<strong>Editing {{ consoleserverport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Console Server Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if consoleserverport %}{{ consoleserverport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if consoleserverport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add {{ component_name|title }}</h1>
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if request.POST.redirect_url %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||
{% endif %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Selected Devices</strong></div>
|
||||
<table class="panel-body table table-hover">
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Type</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for device in selected_devices %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{{ component_name|title }} to Add</strong></div>
|
||||
<div class="panel-body">
|
||||
{% for field in form.visible_fields %}
|
||||
{% render_field field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if module %}Editing {{ module.device }} {{ module }}{% else %}Add a Module to {{ device }}{% endif %}{% endblock %}
|
||||
{% block title %}Create {{ component_type }} ({{ device }}){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@ -18,13 +18,13 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>{% if module %}Editing {{ module.device }} {{ module }}{% else %}Add a Module to {{ device }}{% endif %}</strong>
|
||||
<strong>{{ component_type }}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if module %}{{ module.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
@ -32,12 +32,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if module.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
@ -78,7 +78,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Position (U)</td>
|
||||
<td>Lowest rack unit occupied by the device (optional)</td>
|
||||
<td>Lowest-numbered rack unit occupied by the device (optional)</td>
|
||||
<td>21</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if devicebay.pk %}Editing {{ devicebay.device }} {{ devicebay }}{% else %}Add a Device Bay ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if poweroutlet.pk %}
|
||||
<strong>Editing {{ devicebay }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Device Bay</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if devicebay %}{{ devicebay.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if devicebay.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -2,6 +2,9 @@
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ ip.vrf|default:"Global" }}
|
||||
</td>
|
||||
<td>{{ ip.interface }}</td>
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block extra_actions %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:device_bulk_add_interface' %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -1,23 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Add Interfaces{% endblock %}
|
||||
|
||||
{% block selected_objects_title %}Selected Devices{% endblock %}
|
||||
|
||||
{% block form_title %}Interface(s) to Add{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Type</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for device in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if interface.pk %}Editing {{ interface.device }} {{ interface }}{% else %}Add an Interface ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if interface.pk %}
|
||||
<strong>Editing {{ interface }}</strong>
|
||||
{% else %}
|
||||
<strong>Add an Interface</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if interface %}{{ interface.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if interface.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Add an IP Address{% endblock %}
|
||||
{% block title %}Assign an IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@ -18,10 +18,34 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Add an IP Address
|
||||
<strong>IP Address</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% render_form form %}
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if poweroutlet.pk %}Editing {{ poweroutlet.device }} {{ poweroutlet }}{% else %}Add a Power Outlet ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if poweroutlet.pk %}
|
||||
<strong>Editing {{ poweroutlet }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Power Outlet</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if poweroutlet %}{{ poweroutlet.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if poweroutlet.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if powerport.pk %}Editing {{ powerport.device }} {{ powerport }}{% else %}Add a Power Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if powerport.pk %}
|
||||
<strong>Editing {{ powerport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Power Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if powerport %}{{ powerport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if powerport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -122,7 +122,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<td>{{ rack.u_height }}U</td>
|
||||
<td>{{ rack.u_height }}U ({% if rack.desc_units %}descending{% else %}ascending{% endif %})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Devices</td>
|
||||
@ -189,13 +189,13 @@
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/_rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
|
||||
{% include 'dcim/inc/_rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
{% include 'dcim/_rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
|
||||
{% include 'dcim/inc/_rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,6 +14,7 @@
|
||||
{% render_field form.type %}
|
||||
{% render_field form.width %}
|
||||
{% render_field form.u_height %}
|
||||
{% render_field form.desc_units %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
|
@ -73,10 +73,15 @@
|
||||
<td>Height in rack units</td>
|
||||
<td>42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Descending units</td>
|
||||
<td>Units are numbered top-to-bottom</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42</pre>
|
||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -76,6 +76,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ ipaddress.get_status_class }}">{{ ipaddress.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
@ -91,8 +97,14 @@
|
||||
<td>
|
||||
{% if ipaddress.interface %}
|
||||
<span><a href="{% url 'dcim:device' pk=ipaddress.interface.device.pk %}">{{ ipaddress.interface.device }}</a> ({{ ipaddress.interface }})</span>
|
||||
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_remove' pk=ipaddress.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_assign' pk=ipaddress.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
56
netbox/templates/ipam/ipaddress_assign.html
Normal file
56
netbox/templates/ipam/ipaddress_assign.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Assign IP Address {{ ipaddress }} ({% if ipaddress.vrf %}VRF {{ ipaddress.vrf }}{% else %}Global Table{% endif %})</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
||||
{% endblock %}
|
@ -8,6 +8,7 @@
|
||||
<th>IP Address</th>
|
||||
<th>VRF</th>
|
||||
<th>Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
@ -16,6 +17,7 @@
|
||||
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
||||
<td>{{ ipaddress.vrf|default:"Global" }}</td>
|
||||
<td>{{ ipaddress.tenant }}</td>
|
||||
<td>{{ ipaddress.get_status_display }}</td>
|
||||
<td>{% if ipaddress.interface %}<i class="glyphicon glyphicon-ok text-success" title="{{ ipaddress.interface.device }} {{ ipaddress.interface }}"></i>{% endif %}</td>
|
||||
<td>{{ ipaddress.description }}</td>
|
||||
</tr>
|
||||
|
@ -9,6 +9,7 @@
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% if obj %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
@ -16,8 +17,12 @@
|
||||
<p class="form-control-static">
|
||||
{% if obj.interface %}
|
||||
<a href="{% url 'dcim:device' pk=obj.interface.device.pk %}">{{ obj.interface.device }}</a>
|
||||
<a href="{% url 'ipam:ipaddress_remove' pk=obj.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
|
||||
{% else %}
|
||||
<span>None</span>
|
||||
<span class="text-muted">None</span>
|
||||
{% if obj.pk %}
|
||||
<a href="{% url 'ipam:ipaddress_assign' pk=obj.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@ -25,7 +30,13 @@
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Interface</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ obj.interface }}</p>
|
||||
<p class="form-control-static">
|
||||
{% if obj.interface %}
|
||||
{{ obj.interface }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -43,6 +43,11 @@
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name (optional)</td>
|
||||
@ -66,7 +71,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
8
netbox/templates/ipam/ipaddress_unassign.html
Normal file
8
netbox/templates/ipam/ipaddress_unassign.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Remove {{ ipaddress }} from {{ ipaddress.interface }}?{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Are you sure you want to remove this IP address from <strong>{{ ipaddress.interface.device }} {{ ipaddress.interface }}</strong>?</p>
|
||||
{% endblock %}
|
@ -29,7 +29,7 @@ def login(request):
|
||||
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
messages.info(request, "Logged in as {0}.".format(request.user))
|
||||
messages.info(request, u"Logged in as {}.".format(request.user))
|
||||
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
@ -44,7 +44,7 @@ def login(request):
|
||||
def logout(request):
|
||||
|
||||
auth_logout(request)
|
||||
messages.info(request, "You have logged out.")
|
||||
messages.info(request, u"You have logged out.")
|
||||
return HttpResponseRedirect(reverse('home'))
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ def change_password(request):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, "Your password has been changed successfully.")
|
||||
messages.success(request, u"Your password has been changed successfully.")
|
||||
return redirect('users:profile')
|
||||
|
||||
else:
|
||||
@ -105,7 +105,7 @@ def userkey_edit(request):
|
||||
uk = form.save(commit=False)
|
||||
uk.user = request.user
|
||||
uk.save()
|
||||
messages.success(request, "Your user key has been saved.")
|
||||
messages.success(request, u"Your user key has been saved.")
|
||||
return redirect('users:userkey')
|
||||
|
||||
else:
|
||||
|
@ -11,25 +11,51 @@ from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
EXPANSION_PATTERN = '\[(\d+-\d+)\]'
|
||||
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]'
|
||||
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
|
||||
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
|
||||
|
||||
|
||||
def expand_pattern(string):
|
||||
def expand_numeric_pattern(string):
|
||||
"""
|
||||
Expand a numeric pattern into a list of strings. Examples:
|
||||
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
|
||||
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
|
||||
"""
|
||||
lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
|
||||
lead, pattern, remnant = re.split(NUMERIC_EXPANSION_PATTERN, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x), int(y) + 1):
|
||||
if re.search(EXPANSION_PATTERN, remnant):
|
||||
for string in expand_pattern(remnant):
|
||||
if re.search(NUMERIC_EXPANSION_PATTERN, remnant):
|
||||
for string in expand_numeric_pattern(remnant):
|
||||
yield "{}{}{}".format(lead, i, string)
|
||||
else:
|
||||
yield "{}{}{}".format(lead, i, remnant)
|
||||
|
||||
|
||||
def expand_ipaddress_pattern(string, family):
|
||||
"""
|
||||
Expand an IP address pattern into a list of strings. Examples:
|
||||
'192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
|
||||
'2001:db8:0:[0-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:1::/64', ... '2001:db8:0:ff::/64']
|
||||
"""
|
||||
if family not in [4, 6]:
|
||||
raise Exception("Invalid IP address family: {}".format(family))
|
||||
if family == 4:
|
||||
regex = IP4_EXPANSION_PATTERN
|
||||
base = 10
|
||||
else:
|
||||
regex = IP6_EXPANSION_PATTERN
|
||||
base = 16
|
||||
lead, pattern, remnant = re.split(regex, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x, base), int(y, base) + 1):
|
||||
if re.search(regex, remnant):
|
||||
for string in expand_ipaddress_pattern(remnant, family):
|
||||
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])
|
||||
else:
|
||||
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
|
||||
|
||||
|
||||
def add_blank_choice(choices):
|
||||
"""
|
||||
Add a blank choice to the beginning of a choices list.
|
||||
@ -178,8 +204,28 @@ class ExpandableNameField(forms.CharField):
|
||||
'Example: <code>ge-0/0/[0-47]</code>'
|
||||
|
||||
def to_python(self, value):
|
||||
if re.search(EXPANSION_PATTERN, value):
|
||||
return list(expand_pattern(value))
|
||||
if re.search(NUMERIC_EXPANSION_PATTERN, value):
|
||||
return list(expand_numeric_pattern(value))
|
||||
return [value]
|
||||
|
||||
|
||||
class ExpandableIPAddressField(forms.CharField):
|
||||
"""
|
||||
A field which allows for expansion of IP address ranges
|
||||
Example: '192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExpandableIPAddressField, self).__init__(*args, **kwargs)
|
||||
if not self.help_text:
|
||||
self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\
|
||||
'Example: <code>192.0.2.[1-254]/24</code>'
|
||||
|
||||
def to_python(self, value):
|
||||
# Hackish address family detection but it's all we have to work with
|
||||
if '.' in value and re.search(IP4_EXPANSION_PATTERN, value):
|
||||
return list(expand_ipaddress_pattern(value, 4))
|
||||
elif ':' in value and re.search(IP6_EXPANSION_PATTERN, value):
|
||||
return list(expand_ipaddress_pattern(value, 6))
|
||||
return [value]
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ class ObjectListView(View):
|
||||
filename='netbox_{}'.format(model._meta.verbose_name_plural))
|
||||
return response
|
||||
except TemplateSyntaxError:
|
||||
messages.error(request, "There was an error rendering the selected export template ({})."
|
||||
messages.error(request, u"There was an error rendering the selected export template ({})."
|
||||
.format(et.name))
|
||||
# Fall back to built-in CSV export
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
@ -129,6 +129,13 @@ class ObjectEditView(View):
|
||||
else:
|
||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||
|
||||
def get_cancel_url(self, obj):
|
||||
if hasattr(obj, 'get_absolute_url'):
|
||||
return obj.get_absolute_url()
|
||||
if hasattr(obj, 'get_parent_url'):
|
||||
return obj.get_parent_url()
|
||||
return reverse(self.cancel_url)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
if kwargs:
|
||||
@ -142,7 +149,7 @@ class ObjectEditView(View):
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||
'cancel_url': self.get_cancel_url(obj),
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -174,14 +181,16 @@ class ObjectEditView(View):
|
||||
return redirect(request.path)
|
||||
elif self.success_url:
|
||||
return redirect(self.success_url)
|
||||
else:
|
||||
elif hasattr(obj, 'get_absolute_url'):
|
||||
return redirect(obj.get_absolute_url())
|
||||
elif hasattr(obj, 'get_parent_url'):
|
||||
return redirect(obj.get_parent_url())
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||
'cancel_url': self.get_cancel_url(obj),
|
||||
})
|
||||
|
||||
|
||||
@ -197,6 +206,13 @@ class ObjectDeleteView(View):
|
||||
else:
|
||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||
|
||||
def get_cancel_url(self, obj):
|
||||
if hasattr(obj, 'get_absolute_url'):
|
||||
return obj.get_absolute_url()
|
||||
if hasattr(obj, 'get_parent_url'):
|
||||
return obj.get_parent_url()
|
||||
return reverse('home')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
obj = self.get_object(kwargs)
|
||||
@ -206,7 +222,7 @@ class ObjectDeleteView(View):
|
||||
'obj': obj,
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'cancel_url': obj.get_absolute_url(),
|
||||
'cancel_url': self.get_cancel_url(obj),
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -216,19 +232,24 @@ class ObjectDeleteView(View):
|
||||
if form.is_valid():
|
||||
try:
|
||||
obj.delete()
|
||||
msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
return redirect(self.redirect_url)
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(obj, request, e)
|
||||
return redirect(obj.get_absolute_url())
|
||||
msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
if self.redirect_url:
|
||||
return redirect(self.redirect_url)
|
||||
elif hasattr(obj, 'get_parent_url'):
|
||||
return redirect(obj.get_parent_url())
|
||||
else:
|
||||
return redirect('home')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'cancel_url': obj.get_absolute_url(),
|
||||
'cancel_url': self.get_cancel_url(obj),
|
||||
})
|
||||
|
||||
|
||||
@ -347,7 +368,7 @@ class BulkEditView(View):
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural))
|
||||
messages.warning(request, u"No {} were selected.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
@ -460,7 +481,7 @@ class BulkDeleteView(View):
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
|
||||
messages.warning(request, u"No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
|
Loading…
Reference in New Issue
Block a user