Merge pull request #666 from digitalocean/develop

Release v1.7.0
This commit is contained in:
Jeremy Stretch 2016-11-03 15:12:34 -04:00 committed by GitHub
commit 57ddd5086f
61 changed files with 1003 additions and 904 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.pyc
configuration.py
/netbox/netbox/configuration.py
/netbox/static
.idea
/*.sh
!upgrade.sh

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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 %}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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