mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-29 16:47:46 -06:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6159994552 | ||
|
|
398041c607 | ||
|
|
6ce9f8f291 | ||
|
|
c2c8a139f3 | ||
|
|
698c0decb4 | ||
|
|
ef61c70a9d | ||
|
|
97863115ba | ||
|
|
fa5493a5d8 | ||
|
|
3e9cec3e8e | ||
|
|
943ec0b64b | ||
|
|
8008015082 | ||
|
|
af54d96d30 | ||
|
|
d98aa03e9d | ||
|
|
8d4c686ae2 | ||
|
|
982b9454f8 | ||
|
|
28a2a37ed2 | ||
|
|
3f019732b3 | ||
|
|
007852a48f | ||
|
|
3474697a66 | ||
|
|
4e09b32dd9 | ||
|
|
6dde0f030a | ||
|
|
d154b4cc9e | ||
|
|
7c11fa7b50 | ||
|
|
264bf6c484 | ||
|
|
3854a9d633 | ||
|
|
8bad3aee74 | ||
|
|
a1f624c1cc | ||
|
|
ff0a0df478 | ||
|
|
5dd2f37035 | ||
|
|
862e44e96f | ||
|
|
643b0eaf65 | ||
|
|
0af6df3121 | ||
|
|
e0616d933f | ||
|
|
1e7fdbc79a | ||
|
|
1473d90243 | ||
|
|
32eee0bede | ||
|
|
131436fc20 | ||
|
|
966c188977 | ||
|
|
afba80bff9 | ||
|
|
0d267d97fe | ||
|
|
b0cd372af9 | ||
|
|
e5af4f6f17 | ||
|
|
399a633d9d | ||
|
|
2ef223b5ea | ||
|
|
2cdb527df9 | ||
|
|
fc0e8e2aae | ||
|
|
e5454d6714 | ||
|
|
328958876a | ||
|
|
a7389de109 | ||
|
|
b911ab01d2 | ||
|
|
9153c71cbf | ||
|
|
b44aa9d32e | ||
|
|
bcb1d9af0b | ||
|
|
ef84889a57 | ||
|
|
81c027e7cf | ||
|
|
fd62a248ee | ||
|
|
2c8bea1b59 | ||
|
|
07364abf9e | ||
|
|
20cb13e1bb | ||
|
|
3f3b385de7 | ||
|
|
94b12e506e | ||
|
|
4ec6e52e73 | ||
|
|
88adc5ca86 | ||
|
|
8d9543cb6a |
@@ -9,7 +9,7 @@ python:
|
||||
- "3.5"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pep8
|
||||
- pip install pycodestyle
|
||||
before_script:
|
||||
- psql --version
|
||||
- psql -U postgres -c 'SELECT version();'
|
||||
|
||||
@@ -12,31 +12,37 @@ Download and extract the latest version:
|
||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||
# tar -xzf vX.Y.Z.tar.gz -C /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:
|
||||
|
||||
```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.)
|
||||
|
||||
```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:
|
||||
|
||||
```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:
|
||||
|
||||
```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)
|
||||
|
||||
@@ -19,6 +19,7 @@ from . import serializers
|
||||
|
||||
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Circuit, ['status']),
|
||||
(CircuitTermination, ['term_side']),
|
||||
)
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ class NestedSiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class WritableSiteSerializer(CustomFieldModelSerializer):
|
||||
time_zone = TimeZoneField(required=False)
|
||||
time_zone = TimeZoneField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -233,7 +233,7 @@ class WritableRackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'rack', 'units', 'user', 'description']
|
||||
fields = ['id', 'rack', 'units', 'user', 'tenant', 'description']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
@@ -37,11 +36,12 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Device, ['face', 'status']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Interface, ['form_factor']),
|
||||
(Interface, ['form_factor', 'mode']),
|
||||
(InterfaceConnection, ['connection_status']),
|
||||
(InterfaceTemplate, ['form_factor']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['type', 'width']),
|
||||
(Site, ['status']),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from .models import (
|
||||
RackRole, Region, Site, VirtualChassis
|
||||
)
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
DEVICE_BY_PK_RE = r'{\d+\}'
|
||||
|
||||
INTERFACE_MODE_HELP_TEXT = """
|
||||
Access: One untagged VLAN<br />
|
||||
@@ -112,9 +112,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'description',
|
||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone',
|
||||
'comments',
|
||||
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
@@ -124,6 +123,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
'name': "Full name of the site",
|
||||
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||
'asn': "BGP autonomous system number",
|
||||
'time_zone': "Local time zone",
|
||||
'description': "Short description (will appear in sites list)",
|
||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||
'shipping_address': "If different from the physical address"
|
||||
}
|
||||
@@ -131,7 +132,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class SiteCSVForm(forms.ModelForm):
|
||||
status = CSVChoiceField(
|
||||
choices=DEVICE_STATUS_CHOICES,
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
@@ -165,13 +166,37 @@ class SiteCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(SITE_STATUS_CHOICES), 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(required=False)
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(SITE_STATUS_CHOICES),
|
||||
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:
|
||||
nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone']
|
||||
@@ -705,7 +730,7 @@ class PlatformCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Manufacturer name',
|
||||
error_messages={
|
||||
|
||||
@@ -963,6 +963,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
'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:
|
||||
|
||||
try:
|
||||
@@ -1205,8 +1211,8 @@ class ConsoleServerPortManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
# Pad any trailing digits to effect natural sorting
|
||||
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
||||
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
|
||||
"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
|
||||
'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
|
||||
r"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
@@ -1236,7 +1242,7 @@ class ConsoleServerPort(models.Model):
|
||||
raise ValidationError("Console server ports must be assigned to devices.")
|
||||
device_type = self.device.device_type
|
||||
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
|
||||
))
|
||||
|
||||
@@ -1287,8 +1293,8 @@ class PowerOutletManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
# Pad any trailing digits to effect natural sorting
|
||||
return super(PowerOutletManager, self).get_queryset().extra(select={
|
||||
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
|
||||
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
|
||||
'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
|
||||
r"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
@@ -1318,7 +1324,7 @@ class PowerOutlet(models.Model):
|
||||
raise ValidationError("Power outlets must be assigned to devices.")
|
||||
device_type = self.device.device_type
|
||||
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
|
||||
))
|
||||
|
||||
@@ -1403,7 +1409,7 @@ class Interface(models.Model):
|
||||
if self.device is not None:
|
||||
device_type = self.device.device_type
|
||||
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
|
||||
))
|
||||
|
||||
@@ -1536,6 +1542,18 @@ class InterfaceConnection(models.Model):
|
||||
raise ValidationError({
|
||||
'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:
|
||||
pass
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet):
|
||||
}[method]
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)"
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
|
||||
@@ -11,8 +11,13 @@ def assign_virtualchassis_master(instance, created, **kwargs):
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -7,9 +7,9 @@ from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||
VirtualChassis,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
|
||||
Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
@@ -594,7 +594,7 @@ class InterfaceConnectionTable(BaseTable):
|
||||
interface_b = tables.Column(verbose_name='Interface B')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
model = InterfaceConnection
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||
|
||||
|
||||
|
||||
@@ -41,19 +41,21 @@ class BulkRenameView(View):
|
||||
"""
|
||||
An extendable view for renaming device components in bulk.
|
||||
"""
|
||||
model = None
|
||||
queryset = None
|
||||
form = None
|
||||
template_name = 'dcim/bulk_rename.html'
|
||||
|
||||
def post(self, request):
|
||||
|
||||
model = self.queryset.model
|
||||
|
||||
return_url = request.GET.get('return_url')
|
||||
if not return_url or not is_safe_url(url=return_url, host=request.get_host()):
|
||||
return_url = 'home'
|
||||
|
||||
if '_preview' in request.POST or '_apply' in request.POST:
|
||||
form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
|
||||
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
|
||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||
|
||||
if form.is_valid():
|
||||
for obj in selected_objects:
|
||||
@@ -65,17 +67,17 @@ class BulkRenameView(View):
|
||||
obj.save()
|
||||
messages.success(request, "Renamed {} {}".format(
|
||||
len(selected_objects),
|
||||
self.model._meta.verbose_name_plural
|
||||
model._meta.verbose_name_plural
|
||||
))
|
||||
return redirect(return_url)
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
|
||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type_plural': self.model._meta.verbose_name_plural,
|
||||
'obj_type_plural': model._meta.verbose_name_plural,
|
||||
'selected_objects': selected_objects,
|
||||
'return_url': return_url,
|
||||
})
|
||||
@@ -155,6 +157,7 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_region'
|
||||
cls = Region
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
filter = filters.RegionFilter
|
||||
table = tables.RegionTable
|
||||
default_return_url = 'dcim:region_list'
|
||||
|
||||
@@ -489,6 +492,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
filter = filters.RackReservationFilter
|
||||
table = tables.RackReservationTable
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
@@ -1316,7 +1320,7 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
form = forms.ConsoleServerPortBulkRenameForm
|
||||
|
||||
|
||||
@@ -1600,7 +1604,7 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
model = PowerOutlet
|
||||
queryset = PowerOutlet.objects.all()
|
||||
form = forms.PowerOutletBulkRenameForm
|
||||
|
||||
|
||||
@@ -1676,7 +1680,7 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model = Interface
|
||||
queryset = Interface.objects.order_naturally()
|
||||
form = forms.InterfaceBulkRenameForm
|
||||
|
||||
|
||||
@@ -1783,7 +1787,7 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
|
||||
|
||||
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
model = DeviceBay
|
||||
queryset = DeviceBay.objects.all()
|
||||
form = forms.DeviceBayBulkRenameForm
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class TopologyMapViewSet(ModelViewSet):
|
||||
|
||||
try:
|
||||
data = tmap.render(img_format=img_format)
|
||||
except:
|
||||
except Exception:
|
||||
return HttpResponse(
|
||||
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
|
||||
"installed correctly."
|
||||
|
||||
@@ -43,11 +43,18 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
return queryset.none()
|
||||
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
queryset = queryset.filter(custom_field_values__field__name=self.name)
|
||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||
return queryset.filter(custom_field_values__serialized_value=value)
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value=value
|
||||
)
|
||||
else:
|
||||
return queryset.filter(custom_field_values__serialized_value__icontains=value)
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value__icontains=value
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
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
|
||||
@@ -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()]
|
||||
if not cf.required or bulk_edit or filterable_only:
|
||||
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
|
||||
elif cf.type == CF_TYPE_URL:
|
||||
|
||||
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
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(
|
||||
model_name='customfield',
|
||||
|
||||
@@ -19,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
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'):
|
||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class CustomField(models.Model):
|
||||
default = models.CharField(
|
||||
max_length=100,
|
||||
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(
|
||||
default=100,
|
||||
|
||||
@@ -163,8 +163,8 @@ class IOSSSH(SSHClient):
|
||||
|
||||
sh_ver = self._send('show version').split('\r\n')
|
||||
return {
|
||||
'serial': parse(sh_ver, 'Processor board ID ([^\s]+)'),
|
||||
'description': parse(sh_ver, 'cisco ([^\s]+)')
|
||||
'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'),
|
||||
'description': parse(sh_ver, r'cisco ([^\s]+)')
|
||||
}
|
||||
|
||||
def items(chassis_serial=None):
|
||||
@@ -172,9 +172,9 @@ class IOSSSH(SSHClient):
|
||||
for i in cmd:
|
||||
i_fmt = i.replace('\r\n', ' ')
|
||||
try:
|
||||
m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1)
|
||||
m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1)
|
||||
m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1)
|
||||
m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1)
|
||||
m_pid = re.search(r'PID: ([^\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
|
||||
if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
|
||||
yield {
|
||||
@@ -208,7 +208,7 @@ class OpengearSSH(SSHClient):
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("showserial")
|
||||
serial = stdout.readlines()[0].strip()
|
||||
except:
|
||||
except Exception:
|
||||
raise RuntimeError("Failed to glean chassis serial from device.")
|
||||
# Older models don't provide serial info
|
||||
if serial == "No serial number information available":
|
||||
@@ -217,7 +217,7 @@ class OpengearSSH(SSHClient):
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
|
||||
description = stdout.readlines()[0].split(' ', 1)[1].strip()
|
||||
except:
|
||||
except Exception:
|
||||
raise RuntimeError("Failed to glean chassis description from device.")
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
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 extras.api.views import CustomFieldModelViewSet
|
||||
@@ -98,7 +98,31 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
|
||||
|
||||
# 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
|
||||
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]
|
||||
|
||||
# Determine if the requested number of IPs is available
|
||||
available_ips = list(prefix.get_available_ips())
|
||||
if len(available_ips) < len(requested_ips):
|
||||
available_ips = prefix.get_available_ips()
|
||||
if available_ips.size < len(requested_ips):
|
||||
return Response(
|
||||
{
|
||||
"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
|
||||
available_ips = iter(available_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
|
||||
|
||||
# Initialize the serializer with a list or a single object depending on what was requested
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from netaddr import IPNetwork
|
||||
from netaddr import AddrFormatError, IPNetwork
|
||||
|
||||
from .formfields import IPFormField
|
||||
from . import lookups
|
||||
@@ -26,7 +26,9 @@ class BaseIPField(models.Field):
|
||||
return value
|
||||
try:
|
||||
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)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
import netaddr
|
||||
from netaddr.core import AddrFormatError
|
||||
@@ -233,6 +234,10 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
address = django_filters.CharFilter(
|
||||
method='filter_address',
|
||||
label='Address',
|
||||
)
|
||||
mask_length = django_filters.NumberFilter(
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
@@ -313,6 +318,17 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
except (AddrFormatError, ValueError):
|
||||
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):
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
@@ -508,7 +508,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
||||
|
||||
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
|
||||
|
||||
# Assign this IPAddress as the primary for the associated Device.
|
||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||
if self.cleaned_data['primary_for_parent']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if ipaddress.address.version == 4:
|
||||
@@ -516,14 +516,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
||||
else:
|
||||
parent.primary_ip6 = ipaddress
|
||||
parent.save()
|
||||
|
||||
# Clear assignment as primary for device if set.
|
||||
elif self.cleaned_data['interface']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if ipaddress.address.version == 4 and parent.primary_ip4 == self:
|
||||
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
|
||||
parent.primary_ip4 = None
|
||||
parent.save()
|
||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == self:
|
||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
|
||||
parent.primary_ip6 = None
|
||||
parent.save()
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ class IPAddressAssignTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
|
||||
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
||||
orderable = False
|
||||
|
||||
|
||||
|
||||
@@ -729,8 +729,8 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
|
||||
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
||||
).filter(
|
||||
vrf=form.cleaned_data['vrf'],
|
||||
address__net_host=form.cleaned_data['address'],
|
||||
)
|
||||
address__istartswith=form.cleaned_data['address'],
|
||||
)[:100] # Limit to 100 results
|
||||
table = tables.IPAddressAssignTable(queryset)
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
|
||||
@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
VERSION = '2.3.2'
|
||||
VERSION = '2.3.5'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -268,7 +268,15 @@ SWAGGER_SETTINGS = {
|
||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
]
|
||||
],
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'Authorization',
|
||||
'in': 'header',
|
||||
}
|
||||
},
|
||||
'VALIDATOR_URL': None,
|
||||
}
|
||||
|
||||
|
||||
@@ -281,5 +289,5 @@ INTERNAL_IPS = (
|
||||
|
||||
try:
|
||||
HOSTNAME = socket.gethostname()
|
||||
except:
|
||||
except Exception:
|
||||
HOSTNAME = 'localhost'
|
||||
|
||||
@@ -52,9 +52,9 @@ _patterns = [
|
||||
url(r'^api/secrets/', include('secrets.api.urls')),
|
||||
url(r'^api/tenancy/', include('tenancy.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/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'),
|
||||
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'),
|
||||
url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'),
|
||||
url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'),
|
||||
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
|
||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
|
||||
@@ -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.")
|
||||
try:
|
||||
PKCS1_OAEP.new(key)
|
||||
except:
|
||||
except Exception:
|
||||
raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.")
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class UserKey(CreatedUpdatedModel):
|
||||
raise ValidationError({
|
||||
'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 "
|
||||
"uploading a valid RSA public key in PEM format (no SSH/PGP).")
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ $(document).ready(function() {
|
||||
success: function(json) {
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
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
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
|
||||
@@ -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">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</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>
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -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 %}>
|
||||
{% 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"
|
||||
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 }}
|
||||
{% if u.device.devicebay_count %}
|
||||
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
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>
|
||||
{% add_button 'dcim:rackrole_add' %}
|
||||
{% import_button 'dcim:rackrole_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% render_field form.facility %}
|
||||
{% render_field form.asn %}
|
||||
{% render_field form.time_zone %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</form>
|
||||
{% if table %}
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1" style="margin-top: 20px">
|
||||
<div class="col-md-12" style="margin-top: 20px">
|
||||
<h3>Search Results</h3>
|
||||
{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
|
||||
</div>
|
||||
|
||||
@@ -38,10 +38,10 @@ COLOR_CHOICES = (
|
||||
('607d8b', 'Dark grey'),
|
||||
('111111', 'Black'),
|
||||
)
|
||||
NUMERIC_EXPANSION_PATTERN = '\[((?:\d+[?:,-])+\d+)\]'
|
||||
ALPHANUMERIC_EXPANSION_PATTERN = '\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]'
|
||||
IP4_EXPANSION_PATTERN = '\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
|
||||
IP6_EXPANSION_PATTERN = '\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
|
||||
NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
|
||||
ALPHANUMERIC_EXPANSION_PATTERN = r'\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]'
|
||||
IP4_EXPANSION_PATTERN = r'\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
|
||||
IP6_EXPANSION_PATTERN = r'\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
|
||||
|
||||
|
||||
def parse_numeric_range(string, base=10):
|
||||
@@ -205,7 +205,8 @@ class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
|
||||
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
# Split the delimited string of values into a list
|
||||
value = value[0].split(self.delimiter)
|
||||
if value:
|
||||
value = value[0].split(self.delimiter)
|
||||
return super(ArrayFieldSelectMultiple, self).optgroups(name, value, attrs)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
@@ -406,7 +407,7 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
|
||||
try:
|
||||
if not self.to_field_name:
|
||||
key = 'pk'
|
||||
elif re.match('^\{\d+\}$', value):
|
||||
elif re.match(r'^\{\d+\}$', value):
|
||||
key = 'pk'
|
||||
value = value.strip('{}')
|
||||
else:
|
||||
|
||||
@@ -23,9 +23,9 @@ class NaturalOrderByManager(Manager):
|
||||
id3 = '_{}_{}3'.format(db_table, primary_field)
|
||||
|
||||
queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={
|
||||
id1: "CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)".format(db_table, primary_field),
|
||||
id2: "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field),
|
||||
id3: "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: r"SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".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)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def csv_format(data):
|
||||
for value in data:
|
||||
|
||||
# Represent None or False with empty string
|
||||
if value in [None, False]:
|
||||
if value is None or value is False:
|
||||
csv.append('')
|
||||
continue
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
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 True
|
||||
|
||||
|
||||
@@ -626,8 +626,11 @@ class BulkDeleteView(View):
|
||||
return_url = reverse(self.default_return_url)
|
||||
|
||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
|
||||
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]
|
||||
else:
|
||||
pk_list = self.cls.objects.values_list('pk', flat=True)
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||
from dcim.constants import IFACE_FF_VIRTUAL
|
||||
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
from virtualization.constants import VM_STATUS_CHOICES
|
||||
@@ -133,13 +133,26 @@ class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
|
||||
# VM interfaces
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency
|
||||
class InterfaceVLANSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES)
|
||||
untagged_vlan = InterfaceVLANSerializer()
|
||||
tagged_vlans = InterfaceVLANSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description',
|
||||
'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', 'tagged_vlans',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -157,5 +170,6 @@ class WritableInterfaceSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description',
|
||||
'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan',
|
||||
'tagged_vlans', 'description',
|
||||
]
|
||||
|
||||
@@ -115,7 +115,7 @@ class ClusterView(View):
|
||||
'site', 'rack', 'tenant', 'device_type__manufacturer'
|
||||
)
|
||||
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')
|
||||
|
||||
return render(request, 'virtualization/cluster.html', {
|
||||
@@ -160,6 +160,7 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'virtualization.delete_cluster'
|
||||
cls = Cluster
|
||||
queryset = Cluster.objects.all()
|
||||
filter = filters.ClusterFilter
|
||||
table = tables.ClusterTable
|
||||
default_return_url = 'virtualization:cluster_list'
|
||||
|
||||
|
||||
@@ -23,8 +23,11 @@ fi
|
||||
|
||||
# Check all python source files for PEP 8 compliance, but explicitly
|
||||
# ignore:
|
||||
# - W504: line break after binary operator
|
||||
# - E501: line greater than 80 characters in length
|
||||
pep8 --ignore=E501 netbox/
|
||||
pycodestyle \
|
||||
--ignore=W504,E501 \
|
||||
netbox/
|
||||
RC=$?
|
||||
if [[ $RC != 0 ]]; then
|
||||
echo -e "\n$(info) one or more PEP 8 errors detected, failing build."
|
||||
|
||||
Reference in New Issue
Block a user