Compare commits

...

48 Commits

Author SHA1 Message Date
Jeremy Stretch
6159994552 Merge pull request #2212 from digitalocean/develop
Release v2.3.5
2018-07-02 15:55:25 -04:00
Jeremy Stretch
398041c607 Release v2.3.5 2018-07-02 15:54:09 -04:00
Jeremy Stretch
6ce9f8f291 Merge pull request #2210 from eriktm/develop
Adding Swagger settings to describe API authentication correctly.
2018-07-02 15:50:37 -04:00
Jeremy Stretch
c2c8a139f3 Merge branch 'develop' into develop 2018-07-02 15:45:36 -04:00
Jeremy Stretch
698c0decb4 Fixes #2021: Fix recursion error when viewing API docs under Python 3.4 2018-07-02 15:25:49 -04:00
Jeremy Stretch
ef61c70a9d Fixes 2064: Disable calls to online swagger validator 2018-07-02 14:39:32 -04:00
Jeremy Stretch
97863115ba Merge pull request #2206 from abeutot/switch_to_pycodestyle
Switch to pycodestyle
2018-07-02 13:38:36 -04:00
Anaël Beutot
fa5493a5d8 Update CI to use pycostyle instead of pep8 2018-07-02 19:27:53 +02:00
Jeremy Stretch
3e9cec3e8e Closes #2159: Allow custom choice field to specify a default choice 2018-06-29 16:01:28 -04:00
Erik Hetland
943ec0b64b Adding Swagger settings to describe API authentication correctly. Fixes #1826 2018-06-29 22:01:01 +02:00
Jeremy Stretch
8008015082 Tweaked API error reporting from #2181 2018-06-29 15:18:30 -04:00
Jeremy Stretch
af54d96d30 Fixes #2181: Raise validation error on invalid prefix_length when allocating next-available prefix 2018-06-29 15:10:30 -04:00
Jeremy Stretch
d98aa03e9d Fixes #2173: Fixed IndexError when automaticating allocating IP addresses from large IPv6 prefixes 2018-06-29 14:52:37 -04:00
Jeremy Stretch
8d4c686ae2 Fixes #2192: Prevent a 0U device from being assigned to a rack position 2018-06-29 14:09:20 -04:00
Jeremy Stretch
982b9454f8 Closes #2194: Added 'address' filter to IPAddress model 2018-06-29 13:54:21 -04:00
Jeremy Stretch
28a2a37ed2 Fixes #2191: Added missing static choices to circuits and DCIM API endpoints 2018-06-29 13:17:07 -04:00
Jeremy Stretch
3f019732b3 Merge pull request #2209 from digitalocean/revert-2169-patch-1
Revert "Closes #2168: Add Extreme SummitStack interface form factors"
2018-06-29 12:19:33 -04:00
Jeremy Stretch
007852a48f Revert "Closes #2168: Add Extreme SummitStack interface form factors" 2018-06-29 12:18:49 -04:00
Jeremy Stretch
3474697a66 Merge pull request #2169 from tradiuz/patch-1
Closes #2168: Add Extreme SummitStack interface form factors
2018-06-29 12:18:37 -04:00
Anaël Beutot
4e09b32dd9 Fix pycodestyle errors
Mainly two kind of errors:
* pokemon exceptions
* invalid escape sequences
2018-06-27 17:24:33 +02:00
Jeremy Stretch
6dde0f030a Fixes #2182: ValueError raised when viewing the interface connections table 2018-06-19 13:37:12 -04:00
Jeremy Stretch
d154b4cc9e Merge pull request #2178 from chowell5/add-serial-to-bubble
Add a serial number to the popover in rack elevation number
2018-06-18 13:34:44 -04:00
Chris Howells
7c11fa7b50 Add a serial number to the popover in rack elevation number 2018-06-18 14:35:07 +01:00
tradiuz
264bf6c484 Adding SummitStack-256 2018-06-15 13:43:04 -05:00
tradiuz
3854a9d633 Changes for Issue #2168
Adding support for Extreme Networks SummitStack port types.
2018-06-14 16:59:00 -05:00
Jeremy Stretch
8bad3aee74 Post-release version bump 2018-06-07 16:22:36 -04:00
Jeremy Stretch
a1f624c1cc Merge pull request #2152 from digitalocean/develop
Release v2.3.4
2018-06-07 16:14:18 -04:00
Jeremy Stretch
ff0a0df478 Release v2.3.4 2018-06-07 15:53:05 -04:00
Jeremy Stretch
5dd2f37035 Fixes #2087: Don't overwrite existing vc_position of master device when creating a virtual chassis 2018-06-07 15:32:19 -04:00
Jeremy Stretch
862e44e96f Fixes #2148: Do not force timezone selection when editing sites in bulk 2018-06-07 14:51:27 -04:00
Jeremy Stretch
643b0eaf65 Fixes #2127: Prevent non-conntectable interfaces from being connected 2018-06-07 14:22:56 -04:00
Jeremy Stretch
0af6df3121 Fixes #2150: Fix display of LLDP neighbors when interface name contains a colon 2018-06-07 10:55:30 -04:00
Jeremy Stretch
e0616d933f Merge pull request #2144 from digitalocean/update-site-serializer
Fixes #2143 - PUTs to Site Endpoint Requires Value for time_zone
2018-06-06 11:06:51 -04:00
zmoody
1e7fdbc79a Fixes #2143 - PUTs to Site Endpoint Requires Value for time_zone
Allow null values for `time_zone` field in the writeable serializer for the sites endpoint.
2018-06-05 10:26:33 -05:00
Jeremy Stretch
1473d90243 Merge pull request #2110 from mandarg/fix-error-message
Add "does" to error messages
2018-05-24 15:19:43 -04:00
Mandar Gokhale
32eee0bede Add "does" to error messages
Those error messages looked a bit strange when I got them, hence the
fix.
2018-05-23 17:41:52 -04:00
Reimann, Timo
131436fc20 Changed upgrading documentation for ease of use 2018-05-22 16:20:10 -04:00
Jeremy Stretch
966c188977 Merge pull request #1939 from dougthor42/patch-1
Add note about copying reports to `upgrading.md`
2018-05-22 16:16:43 -04:00
Jeremy Stretch
afba80bff9 Merge pull request #2083 from Grokzen/add_rack_role_export
Add missing export button to rack roles list view.
2018-05-22 15:52:50 -04:00
Jeremy Stretch
0d267d97fe Fixes #2075: Enable tenant assignment when creating a rack reservation via the API 2018-05-22 14:09:06 -04:00
Jeremy Stretch
b0cd372af9 Fixes #2066: Catch AddrFormatError on invalid IP addresses 2018-05-22 13:56:11 -04:00
Jeremy Stretch
e5af4f6f17 Fixes #2093: Fix link to circuit termination in device interfaces table 2018-05-21 17:31:43 -04:00
Jeremy Stretch
399a633d9d Post-release version bump 2018-05-21 16:50:31 -04:00
Jeremy Stretch
2ef223b5ea Merge pull request #2099 from eriktm/2098-permission-typo
Fixing typo in permission check for ClusterView.
2018-05-21 16:20:09 -04:00
Erik Hetland
2cdb527df9 Fixing typo in permission check for ClusterView. 2018-05-19 11:50:03 +02:00
Grokzen
fc0e8e2aae Add export button to rack roles list view. 2018-05-08 16:06:53 +02:00
Jeremy Stretch
e5454d6714 Post-release version bump 2018-04-19 11:17:17 -04:00
Douglas Thor
8d9543cb6a Add note about copying reports to upgrading.md
The `upgrading.md` file does not mention reports. If the user created reports in the old version's default directory (`./netbox/reports`), then the reports will not be transferred to the new version.
2018-03-01 15:05:51 -08:00
33 changed files with 200 additions and 83 deletions

View File

@@ -9,7 +9,7 @@ python:
- "3.5" - "3.5"
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pep8 - pip install pycodestyle
before_script: before_script:
- psql --version - psql --version
- psql -U postgres -c 'SELECT version();' - psql -U postgres -c 'SELECT version();'

View File

@@ -12,31 +12,37 @@ Download and extract the latest version:
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz # wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt # tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/ # cd /opt/
# ln -sf netbox-X.Y.Z/ netbox # ln -sfn netbox-X.Y.Z/ netbox
``` ```
Copy the 'configuration.py' you created when first installing to the new version: Copy the 'configuration.py' you created when first installing to the new version:
```no-highlight ```no-highlight
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py # cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
``` ```
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
```no-highlight ```no-highlight
# cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ # cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/
```
Also make sure to copy over any reports that you've made. Note that if you made them in a separate directory (`/opt/netbox-reports` for example), then you will not need to copy them - the config file that you copied earlier will point to the correct location.
```no-highlight
# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/
``` ```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```no-highlight ```no-highlight
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py # cp netbox-X.Y.Z/gunicorn_config.py netbox/gunicorn_config.py
``` ```
Copy the LDAP configuration if using LDAP: Copy the LDAP configuration if using LDAP:
```no-highlight ```no-highlight
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py # cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py
``` ```
## Option B: Clone the Git Repository (latest master release) ## Option B: Clone the Git Repository (latest master release)

View File

@@ -19,6 +19,7 @@ from . import serializers
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Circuit, ['status']),
(CircuitTermination, ['term_side']), (CircuitTermination, ['term_side']),
) )

View File

@@ -80,7 +80,7 @@ class NestedSiteSerializer(serializers.ModelSerializer):
class WritableSiteSerializer(CustomFieldModelSerializer): class WritableSiteSerializer(CustomFieldModelSerializer):
time_zone = TimeZoneField(required=False) time_zone = TimeZoneField(required=False, allow_null=True)
class Meta: class Meta:
model = Site model = Site
@@ -233,7 +233,7 @@ class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'rack', 'units', 'user', 'description'] fields = ['id', 'rack', 'units', 'user', 'tenant', 'description']
# #

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
@@ -37,11 +36,12 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Device, ['face', 'status']), (Device, ['face', 'status']),
(ConsolePort, ['connection_status']), (ConsolePort, ['connection_status']),
(Interface, ['form_factor']), (Interface, ['form_factor', 'mode']),
(InterfaceConnection, ['connection_status']), (InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']), (InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']), (PowerPort, ['connection_status']),
(Rack, ['type', 'width']), (Rack, ['type', 'width']),
(Site, ['status']),
) )

View File

@@ -34,7 +34,7 @@ from .models import (
RackRole, Region, Site, VirtualChassis RackRole, Region, Site, VirtualChassis
) )
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = r'{\d+\}'
INTERFACE_MODE_HELP_TEXT = """ INTERFACE_MODE_HELP_TEXT = """
Access: One untagged VLAN<br /> Access: One untagged VLAN<br />
@@ -166,13 +166,37 @@ class SiteCSVForm(forms.ModelForm):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(
status = forms.ChoiceField(choices=add_blank_choice(SITE_STATUS_CHOICES), required=False, initial='') queryset=Site.objects.all(),
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False) widget=forms.MultipleHiddenInput
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) )
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') status = forms.ChoiceField(
description = forms.CharField(max_length=100, required=False) choices=add_blank_choice(SITE_STATUS_CHOICES),
time_zone = TimeZoneFormField(required=False) required=False,
initial=''
)
region = TreeNodeChoiceField(
queryset=Region.objects.all(),
required=False
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
asn = forms.IntegerField(
min_value=1,
max_value=4294967295,
required=False,
label='ASN'
)
description = forms.CharField(
max_length=100,
required=False
)
time_zone = TimeZoneFormField(
choices=add_blank_choice(TimeZoneFormField().choices),
required=False
)
class Meta: class Meta:
nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone'] nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone']

View File

@@ -963,6 +963,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'face': "Must specify rack face when defining rack position.", 'face': "Must specify rack face when defining rack position.",
}) })
# Prevent 0U devices from being assigned to a specific position
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type)
})
if self.rack: if self.rack:
try: try:
@@ -1205,8 +1211,8 @@ class ConsoleServerPortManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting # Pad any trailing digits to effect natural sorting
return super(ConsoleServerPortManager, self).get_queryset().extra(select={ return super(ConsoleServerPortManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), " 'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))", r"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')
@@ -1236,7 +1242,7 @@ class ConsoleServerPort(models.Model):
raise ValidationError("Console server ports must be assigned to devices.") raise ValidationError("Console server ports must be assigned to devices.")
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_console_server: if not device_type.is_console_server:
raise ValidationError("The {} {} device type not support assignment of console server ports.".format( raise ValidationError("The {} {} device type does not support assignment of console server ports.".format(
device_type.manufacturer, device_type device_type.manufacturer, device_type
)) ))
@@ -1287,8 +1293,8 @@ class PowerOutletManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting # Pad any trailing digits to effect natural sorting
return super(PowerOutletManager, self).get_queryset().extra(select={ return super(PowerOutletManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), " 'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))", r"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')
@@ -1318,7 +1324,7 @@ class PowerOutlet(models.Model):
raise ValidationError("Power outlets must be assigned to devices.") raise ValidationError("Power outlets must be assigned to devices.")
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_pdu: if not device_type.is_pdu:
raise ValidationError("The {} {} device type not support assignment of power outlets.".format( raise ValidationError("The {} {} device type does not support assignment of power outlets.".format(
device_type.manufacturer, device_type device_type.manufacturer, device_type
)) ))
@@ -1403,7 +1409,7 @@ class Interface(models.Model):
if self.device is not None: if self.device is not None:
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_network_device: if not device_type.is_network_device:
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format( raise ValidationError("The {} {} device type does not support assignment of network interfaces.".format(
device_type.manufacturer, device_type device_type.manufacturer, device_type
)) ))
@@ -1536,6 +1542,18 @@ class InterfaceConnection(models.Model):
raise ValidationError({ raise ValidationError({
'interface_b': "Cannot connect an interface to itself." 'interface_b': "Cannot connect an interface to itself."
}) })
if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'interface_a': '{} is not a connectable interface type.'.format(
self.interface_a.get_form_factor_display()
)
})
if self.interface_b.form_factor in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'interface_b': '{} is not a connectable interface type.'.format(
self.interface_b.get_form_factor_display()
)
})
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass

View File

@@ -11,8 +11,13 @@ def assign_virtualchassis_master(instance, created, **kwargs):
""" """
When a VirtualChassis is created, automatically assign its master device to the VC. When a VirtualChassis is created, automatically assign its master device to the VC.
""" """
# Default to 1 but don't overwrite an existing position (see #2087)
if instance.master.vc_position is not None:
vc_position = instance.master.vc_position
else:
vc_position = 1
if created: if created:
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=1) Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
@receiver(pre_delete, sender=VirtualChassis) @receiver(pre_delete, sender=VirtualChassis)

View File

@@ -7,9 +7,9 @@ from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
VirtualChassis, RackReservation, Region, Site, VirtualChassis,
) )
REGION_LINK = """ REGION_LINK = """
@@ -594,7 +594,7 @@ class InterfaceConnectionTable(BaseTable):
interface_b = tables.Column(verbose_name='Interface B') interface_b = tables.Column(verbose_name='Interface B')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = InterfaceConnection
fields = ('device_a', 'interface_a', 'device_b', 'interface_b') fields = ('device_a', 'interface_a', 'device_b', 'interface_b')

View File

@@ -157,6 +157,7 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region' permission_required = 'dcim.delete_region'
cls = Region cls = Region
queryset = Region.objects.annotate(site_count=Count('sites')) queryset = Region.objects.annotate(site_count=Count('sites'))
filter = filters.RegionFilter
table = tables.RegionTable table = tables.RegionTable
default_return_url = 'dcim:region_list' default_return_url = 'dcim:region_list'
@@ -491,6 +492,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
cls = RackReservation cls = RackReservation
filter = filters.RackReservationFilter
table = tables.RackReservationTable table = tables.RackReservationTable
default_return_url = 'dcim:rackreservation_list' default_return_url = 'dcim:rackreservation_list'

View File

@@ -99,7 +99,7 @@ class TopologyMapViewSet(ModelViewSet):
try: try:
data = tmap.render(img_format=img_format) data = tmap.render(img_format=img_format)
except: except Exception:
return HttpResponse( return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been " "There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly." "installed correctly."

View File

@@ -4,6 +4,7 @@ from collections import OrderedDict
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
@@ -53,7 +54,14 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only: if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices choices = [(None, '---------')] + choices
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required) # Check for a default choice
default_choice = None
if initial:
try:
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
# URL # URL
elif cf.type == CF_TYPE_URL: elif cf.type == CF_TYPE_URL:

View File

@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='customfield', model_name='customfield',
name='default', name='default',
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100), field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100),
), ),
migrations.AlterField( migrations.AlterField(
model_name='customfield', model_name='customfield',

View File

@@ -19,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()") cursor.execute("SELECT VERSION()")
row = cursor.fetchone() row = cursor.fetchone()
pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1) pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'): if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version)) raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))

View File

@@ -91,7 +91,7 @@ class CustomField(models.Model):
default = models.CharField( default = models.CharField(
max_length=100, max_length=100,
blank=True, blank=True,
help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.' help_text='Default value for the field. Use "true" or "false" for booleans.'
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=100, default=100,

View File

@@ -163,8 +163,8 @@ class IOSSSH(SSHClient):
sh_ver = self._send('show version').split('\r\n') sh_ver = self._send('show version').split('\r\n')
return { return {
'serial': parse(sh_ver, 'Processor board ID ([^\s]+)'), 'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'),
'description': parse(sh_ver, 'cisco ([^\s]+)') 'description': parse(sh_ver, r'cisco ([^\s]+)')
} }
def items(chassis_serial=None): def items(chassis_serial=None):
@@ -172,9 +172,9 @@ class IOSSSH(SSHClient):
for i in cmd: for i in cmd:
i_fmt = i.replace('\r\n', ' ') i_fmt = i.replace('\r\n', ' ')
try: try:
m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1) m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1)
m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1) m_pid = re.search(r'PID: ([^\s]+)', i_fmt).group(1)
m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1) m_serial = re.search(r'SN: ([^\s]+)', i_fmt).group(1)
# Omit built-in items and those with no PID # Omit built-in items and those with no PID
if m_serial != chassis_serial and m_pid.lower() != 'unspecified': if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
yield { yield {
@@ -208,7 +208,7 @@ class OpengearSSH(SSHClient):
try: try:
stdin, stdout, stderr = self.ssh.exec_command("showserial") stdin, stdout, stderr = self.ssh.exec_command("showserial")
serial = stdout.readlines()[0].strip() serial = stdout.readlines()[0].strip()
except: except Exception:
raise RuntimeError("Failed to glean chassis serial from device.") raise RuntimeError("Failed to glean chassis serial from device.")
# Older models don't provide serial info # Older models don't provide serial info
if serial == "No serial number information available": if serial == "No serial number information available":
@@ -217,7 +217,7 @@ class OpengearSSH(SSHClient):
try: try:
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model") stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
description = stdout.readlines()[0].split(' ', 1)[1].strip() description = stdout.readlines()[0].split(' ', 1)[1].strip()
except: except Exception:
raise RuntimeError("Failed to glean chassis description from device.") raise RuntimeError("Failed to glean chassis description from device.")
return { return {

View File

@@ -4,7 +4,7 @@ from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
@@ -98,7 +98,31 @@ class PrefixViewSet(CustomFieldModelViewSet):
requested_prefixes = request.data if isinstance(request.data, list) else [request.data] requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
# Allocate prefixes to the requested objects based on availability within the parent # Allocate prefixes to the requested objects based on availability within the parent
for requested_prefix in requested_prefixes: for i, requested_prefix in enumerate(requested_prefixes):
# Validate requested prefix size
error_msg = None
if 'prefix_length' not in requested_prefix:
error_msg = "Item {}: prefix_length field missing".format(i)
elif not isinstance(requested_prefix['prefix_length'], int):
error_msg = "Item {}: Invalid prefix length ({})".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 4 and requested_prefix['prefix_length'] > 32:
error_msg = "Item {}: Invalid prefix length ({}) for IPv4".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 6 and requested_prefix['prefix_length'] > 128:
error_msg = "Item {}: Invalid prefix length ({}) for IPv6".format(
i, requested_prefix['prefix_length']
)
if error_msg:
return Response(
{
"detail": error_msg
},
status=status.HTTP_400_BAD_REQUEST
)
# Find the first available prefix equal to or larger than the requested size # Find the first available prefix equal to or larger than the requested size
for available_prefix in available_prefixes.iter_cidrs(): for available_prefix in available_prefixes.iter_cidrs():
@@ -160,8 +184,8 @@ class PrefixViewSet(CustomFieldModelViewSet):
requested_ips = request.data if isinstance(request.data, list) else [request.data] requested_ips = request.data if isinstance(request.data, list) else [request.data]
# Determine if the requested number of IPs is available # Determine if the requested number of IPs is available
available_ips = list(prefix.get_available_ips()) available_ips = prefix.get_available_ips()
if len(available_ips) < len(requested_ips): if available_ips.size < len(requested_ips):
return Response( return Response(
{ {
"detail": "An insufficient number of IP addresses are available within the prefix {} ({} " "detail": "An insufficient number of IP addresses are available within the prefix {} ({} "
@@ -171,8 +195,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
) )
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix # Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
available_ips = iter(available_ips)
for requested_ip in requested_ips: for requested_ip in requested_ips:
requested_ip['address'] = available_ips.pop(0) requested_ip['address'] = next(available_ips)
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
# Initialize the serializer with a list or a single object depending on what was requested # Initialize the serializer with a list or a single object depending on what was requested

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from netaddr import IPNetwork from netaddr import AddrFormatError, IPNetwork
from .formfields import IPFormField from .formfields import IPFormField
from . import lookups from . import lookups
@@ -26,7 +26,9 @@ class BaseIPField(models.Field):
return value return value
try: try:
return IPNetwork(value) return IPNetwork(value)
except ValueError as e: except AddrFormatError as e:
raise ValidationError("Invalid IP address format: {}".format(value))
except (TypeError, ValueError) as e:
raise ValidationError(e) raise ValidationError(e)
def get_prep_value(self, value): def get_prep_value(self, value):

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
import netaddr import netaddr
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@@ -233,6 +234,10 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search_by_parent', method='search_by_parent',
label='Parent prefix', label='Parent prefix',
) )
address = django_filters.CharFilter(
method='filter_address',
label='Address',
)
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
@@ -313,6 +318,17 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
def filter_address(self, queryset, name, value):
if not value.strip():
return queryset
try:
# Match address and subnet mask
if '/' in value:
return queryset.filter(address=value)
return queryset.filter(address__net_host=value)
except ValidationError:
return queryset.none()
def filter_mask_length(self, queryset, name, value): def filter_mask_length(self, queryset, name, value):
if not value: if not value:
return queryset return queryset

View File

@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning DeprecationWarning
) )
VERSION = '2.3.3' VERSION = '2.3.5'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -268,7 +268,15 @@ SWAGGER_SETTINGS = {
'utilities.custom_inspectors.NullablePaginatorInspector', 'utilities.custom_inspectors.NullablePaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination', 'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
] ],
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
}
},
'VALIDATOR_URL': None,
} }
@@ -281,5 +289,5 @@ INTERNAL_IPS = (
try: try:
HOSTNAME = socket.gethostname() HOSTNAME = socket.gethostname()
except: except Exception:
HOSTNAME = 'localhost' HOSTNAME = 'localhost'

View File

@@ -52,9 +52,9 @@ _patterns = [
url(r'^api/secrets/', include('secrets.api.urls')), url(r'^api/secrets/', include('secrets.api.urls')),
url(r'^api/tenancy/', include('tenancy.api.urls')), url(r'^api/tenancy/', include('tenancy.api.urls')),
url(r'^api/virtualization/', include('virtualization.api.urls')), url(r'^api/virtualization/', include('virtualization.api.urls')),
url(r'^api/docs/$', schema_view.with_ui('swagger', cache_timeout=None), name='api_docs'), url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'),
url(r'^api/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'), url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'),
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'), url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),

View File

@@ -26,7 +26,7 @@ def validate_rsa_key(key, is_secret=True):
raise forms.ValidationError("This looks like a private key. Please provide your public RSA key.") raise forms.ValidationError("This looks like a private key. Please provide your public RSA key.")
try: try:
PKCS1_OAEP.new(key) PKCS1_OAEP.new(key)
except: except Exception:
raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.") raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.")

View File

@@ -87,7 +87,7 @@ class UserKey(CreatedUpdatedModel):
raise ValidationError({ raise ValidationError({
'public_key': "Invalid RSA key format." 'public_key': "Invalid RSA key format."
}) })
except: except Exception:
raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're " 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).") "uploading a valid RSA public key in PEM format (no SSH/PGP).")

View File

@@ -53,7 +53,7 @@ $(document).ready(function() {
success: function(json) { success: function(json) {
$.each(json['get_lldp_neighbors'], function(iface, neighbors) { $.each(json['get_lldp_neighbors'], function(iface, neighbors) {
var neighbor = neighbors[0]; var neighbor = neighbors[0];
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1")); var row = $('#' + iface.split(".")[0].replace(/([\/:])/g, "\\$1"));
// Glean configured hostnames/interfaces from the DOM // Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data'); var configured_device = row.children('td.configured_device').attr('data');

View File

@@ -105,7 +105,7 @@
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected"> <button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</button> </button>
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}&return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Edit circuit termination"> <a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Edit circuit termination">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}

View File

@@ -26,7 +26,7 @@
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}> <li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
{% ifequal u.device.face face_id %} {% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true" <a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}"> data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
{{ u.device.name|default:u.device.device_role }} {{ u.device.name|default:u.device.device_role }}
{% if u.device.devicebay_count %} {% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }}) ({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})

View File

@@ -1,18 +1,13 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_rackrole %} {% if perms.dcim.add_rackrole %}
<a href="{% url 'dcim:rackrole_add' %}" class="btn btn-primary"> {% add_button 'dcim:rackrole_add' %}
<span class="fa fa-plus" aria-hidden="true"></span> {% import_button 'dcim:rackrole_import' %}
Add a rack role
</a>
<a href="{% url 'dcim:rackrole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import rack roles
</a>
{% endif %} {% endif %}
{% export_button content_type %}
</div> </div>
<h1>{% block title %}Rack Roles{% endblock %}</h1> <h1>{% block title %}Rack Roles{% endblock %}</h1>
<div class="row"> <div class="row">

View File

@@ -38,10 +38,10 @@ COLOR_CHOICES = (
('607d8b', 'Dark grey'), ('607d8b', 'Dark grey'),
('111111', 'Black'), ('111111', 'Black'),
) )
NUMERIC_EXPANSION_PATTERN = '\[((?:\d+[?:,-])+\d+)\]' NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
ALPHANUMERIC_EXPANSION_PATTERN = '\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]' ALPHANUMERIC_EXPANSION_PATTERN = r'\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]'
IP4_EXPANSION_PATTERN = '\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]' IP4_EXPANSION_PATTERN = r'\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]' IP6_EXPANSION_PATTERN = r'\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
def parse_numeric_range(string, base=10): def parse_numeric_range(string, base=10):
@@ -407,7 +407,7 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
try: try:
if not self.to_field_name: if not self.to_field_name:
key = 'pk' key = 'pk'
elif re.match('^\{\d+\}$', value): elif re.match(r'^\{\d+\}$', value):
key = 'pk' key = 'pk'
value = value.strip('{}') value = value.strip('{}')
else: else:

View File

@@ -23,9 +23,9 @@ class NaturalOrderByManager(Manager):
id3 = '_{}_{}3'.format(db_table, primary_field) id3 = '_{}_{}3'.format(db_table, primary_field)
queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={ queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={
id1: "CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)".format(db_table, primary_field), id1: r"CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)".format(db_table, primary_field),
id2: "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field), id2: r"SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field),
id3: "CAST(SUBSTRING({}.{} FROM '(\d{{1,9}})$') AS integer)".format(db_table, primary_field), id3: r"CAST(SUBSTRING({}.{} FROM '(\d{{1,9}})$') AS integer)".format(db_table, primary_field),
}) })
ordering = fields[0:-1] + (id1, id2, id3) ordering = fields[0:-1] + (id1, id2, id3)

View File

@@ -15,7 +15,7 @@ class EnhancedURLValidator(URLValidator):
A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1 A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
""" """
def __contains__(self, item): def __contains__(self, item):
if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()): if not item or not re.match(r'^[a-z][0-9a-z+\-.]*$', item.lower()):
return False return False
return True return True

View File

@@ -626,8 +626,11 @@ class BulkDeleteView(View):
return_url = reverse(self.default_return_url) return_url = reverse(self.default_return_url)
# Are we deleting *all* objects in the queryset or just a selected subset? # Are we deleting *all* objects in the queryset or just a selected subset?
if request.POST.get('_all') and self.filter is not None: if request.POST.get('_all'):
if self.filter is not None:
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs] pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
else:
pk_list = self.cls.objects.values_list('pk', flat=True)
else: else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')] pk_list = [int(pk) for pk in request.POST.getlist('pk')]

View File

@@ -115,7 +115,7 @@ class ClusterView(View):
'site', 'rack', 'tenant', 'device_type__manufacturer' 'site', 'rack', 'tenant', 'device_type__manufacturer'
) )
device_table = DeviceTable(list(devices), orderable=False) device_table = DeviceTable(list(devices), orderable=False)
if request.user.has_perm('virtualization:change_cluster'): if request.user.has_perm('virtualization.change_cluster'):
device_table.columns.show('pk') device_table.columns.show('pk')
return render(request, 'virtualization/cluster.html', { return render(request, 'virtualization/cluster.html', {
@@ -160,6 +160,7 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_cluster' permission_required = 'virtualization.delete_cluster'
cls = Cluster cls = Cluster
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
filter = filters.ClusterFilter
table = tables.ClusterTable table = tables.ClusterTable
default_return_url = 'virtualization:cluster_list' default_return_url = 'virtualization:cluster_list'

View File

@@ -23,8 +23,11 @@ fi
# Check all python source files for PEP 8 compliance, but explicitly # Check all python source files for PEP 8 compliance, but explicitly
# ignore: # ignore:
# - W504: line break after binary operator
# - E501: line greater than 80 characters in length # - E501: line greater than 80 characters in length
pep8 --ignore=E501 netbox/ pycodestyle \
--ignore=W504,E501 \
netbox/
RC=$? RC=$?
if [[ $RC != 0 ]]; then if [[ $RC != 0 ]]; then
echo -e "\n$(info) one or more PEP 8 errors detected, failing build." echo -e "\n$(info) one or more PEP 8 errors detected, failing build."