Compare commits

...

63 Commits

Author SHA1 Message Date
Jeremy Stretch
a85e6370a8 Merge pull request #2275 from digitalocean/develop
Release v2.3.7
2018-07-26 14:29:15 -04:00
Jeremy Stretch
0497539ef2 Release v2.3.7 2018-07-26 14:24:16 -04:00
Jeremy Stretch
431361efad Introduced purpose-specific GitHub issue templates 2018-07-26 12:17:16 -04:00
Jeremy Stretch
e82bf66a76 ExceptionHandlingMiddleware: Use server_error view for custom templates 2018-07-23 23:12:41 -04:00
Jeremy Stretch
c8a73b5b15 Fixes #2266: Permit additional logging of exceptions beyond custom middleware 2018-07-23 23:00:09 -04:00
Jeremy Stretch
b518258e6d Closes #2250: Include stat counters on report result navigation 2018-07-23 16:10:46 -04:00
Jeremy Stretch
a1d45023ab Fixes #2256: Prevent navigation overlap when jumping to test results on report page 2018-07-23 15:50:44 -04:00
Jeremy Stretch
ba3ae0d80a Fixes #2257: Corrected casting of RIR utilization stats as floats 2018-07-23 14:52:51 -04:00
Jeremy Stretch
d04727f4b5 Fixes #2255: Corrected display of report results in report list 2018-07-20 09:39:55 -04:00
Jeremy Stretch
93ce0ce670 Further reiterated the policy for pull requests 2018-07-18 16:14:57 -04:00
Jeremy Stretch
c2573774bf Fixes #2222: IP addresses created via the available-ips API endpoint should have the same mask as their parent prefix (not /32) 2018-07-18 15:27:45 -04:00
Jeremy Stretch
6e037e91d3 Fixes #2202: Ditched half-baked concept of tenancy inheritance via VRF 2018-07-18 15:10:12 -04:00
Jeremy Stretch
d665d4d62a Fixes #1992: Isolate errors when one of multiple NAPALM methods fails 2018-07-18 14:46:15 -04:00
Jeremy Stretch
29d9b32b67 Fixes #1977: Don't default master vc_position to 1 when creating a new virtual chassis 2018-07-18 14:17:35 -04:00
Jeremy Stretch
00d218118c Fixes #2231: Remove get_absolute_url() from DeviceRole 2018-07-18 11:24:36 -04:00
Jeremy Stretch
02b6ffd59a Added note about passphrase-protected keys (#2189) 2018-07-18 11:03:22 -04:00
Jeremy Stretch
aa0e4406eb Merge pull request #2167 from lampwins/feature/2166
implements #2166 - asset tag partial string search
2018-07-18 10:40:12 -04:00
Jeremy Stretch
786f389be8 Post-release version bump 2018-07-16 11:56:12 -04:00
Jeremy Stretch
09a03565d7 Merge pull request #2244 from digitalocean/develop
Release v2.3.6
2018-07-16 11:54:12 -04:00
Jeremy Stretch
456b058462 Release v2.3.6 2018-07-16 11:52:12 -04:00
Jeremy Stretch
ecaba5b32e Merge pull request #2230 from digitalocean/2125-device-bay-status
Fixes #2125 - Show child status in device bay list
2018-07-16 11:47:16 -04:00
Jeremy Stretch
9f4c77d6d7 Merge pull request #2232 from mmahacek/patch-1
Update sample report in documentation
2018-07-16 11:46:10 -04:00
Jeremy Stretch
1fb67b791f Fixes #2239: Pin django-filter to version 1.1.0 2018-07-16 11:39:37 -04:00
mmahacek
a26d1812c2 Update sample report
Reference to STATUS_ACTIVE does not work in the current version.  Needs to be changed to DEVICE_STATUS_ACTIVE.
2018-07-11 11:52:33 -07:00
zmoody
b6e354085e Fixes #2125 - Show child status in device bay list
Exposes devicebay.installed_device.status in the parent device detail view.
2018-07-10 20:40:48 -05:00
Jeremy Stretch
108e9722fa Fixes #2214: Fix bug when assigning a VLAN to an interface on a VM in a cluster with no assigned site 2018-07-05 13:28:26 -04:00
Jeremy Stretch
72cb1cbfff Queryset fixes for virtual chassis 2018-07-05 13:20:27 -04:00
Jeremy Stretch
ed84c4b210 Merge pull request #2115 from DanSheps/develop
Added VirtualChassis Searching
2018-07-05 13:15:57 -04:00
Jeremy Stretch
77518eaf69 Merge pull request #2218 from alexjhart/develop
More verbose LDAP nested groups documentation
2018-07-05 13:11:56 -04:00
Jeremy Stretch
4bd36f0ea9 Closes #2062: Added a note about parent/child device type role 2018-07-05 12:02:32 -04:00
Jeremy Stretch
b19bf791a4 Closes #2138: Added documentation for filtering on custom fields 2018-07-05 11:58:07 -04:00
Alex Hart
f70b7cab21 More verbose LDAP nested groups documentation 2018-07-03 15:53:58 -07:00
Jeremy Stretch
b10635a9b1 Added housekeeping as an issue category 2018-07-02 16:39:38 -04:00
Jeremy Stretch
302c14186a Post-release version bump 2018-07-02 15:55:46 -04:00
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
John Anderson
82189de78e implements #2166 - asset tag partial string search 2018-06-14 13:17:06 -04:00
Jeremy Stretch
8bad3aee74 Post-release version bump 2018-06-07 16:22:36 -04:00
dansheps
acc59a9da5 Fix PEP8 2018-05-24 16:03:13 -05:00
dansheps
03ce4bdfca Added VirtualChassis Searching 2018-05-24 15:27:09 -05:00
52 changed files with 466 additions and 251 deletions

View File

@@ -1,48 +0,0 @@
<!--
Before opening a new issue, please search through the existing issues to
see if your topic has already been addressed. Note that you may need to
remove the "is:open" filter from the search bar to include closed issues.
Check the appropriate type for your issue below by placing an x between the
brackets. For assistance with installation issues, or for any other issues
other than those listed below, please raise your topic for discussion on
our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please note that issues which do not fall under any of the below categories
will be closed. Due to an excessive backlog of feature requests, we are
not currently accepting any proposals which extend NetBox's feature scope.
Do not prepend any sort of tag to your issue's title. An administrator will
review your issue and assign labels as appropriate.
--->
### Issue type
[ ] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report <!-- Unexpected or erroneous behavior -->
[ ] Documentation <!-- A modification to the documentation -->
<!--
Please describe the environment in which you are running NetBox. (Be sure
to verify that you are running the latest stable release of NetBox before
submitting a bug report.) If you are submitting a bug report and have made
any changes to the code base, please first validate that your bug can be
recreated while running an official release.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!--
BUG REPORTS must include:
* A list of the steps needed for someone else to reproduce the bug
* A description of the expected and observed behavior
* Any relevant error messages (screenshots may also help)
FEATURE REQUESTS must include:
* A detailed description of the proposed functionality
* A use case for the new feature
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
-->
### Description

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: :bug: Bug Report
about: Report a reproducible bug in the current release of NetBox
---
<!--
NOTE: This form is only for reproducible bugs. If you need assistance with
NetBox installation, or if you have a general question, DO NOT open an
issue. Instead, post to our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.3.6 -->
<!--
Describe in detail the steps that someone else can take to reproduce this
bug using the current stable release of NetBox (or the current beta release
where applicable).
-->
### Steps to Reproduce
<!-- What did you expect to happen? -->
### Expected Behavior
<!-- What happened instead? -->
### Observed Behavior

View File

@@ -0,0 +1,17 @@
---
name: :book: Documentation Change
about: Suggest an addition or modification to the NetBox documentation
---
<!--
Please indicate the nature of the change by placing an X in one of the
boxes below.
-->
### Change Type
[ ] Addition
[ ] Correction
[ ] Deprecation
[ ] Cleanup (formatting, typos, etc.)
<!-- Describe the proposed change(s). -->
### Proposed Changes

View File

@@ -0,0 +1,53 @@
---
name: :new: Feature Request
about: Propose a new NetBox feature or enhancement
---
<!--
NOTE: This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please post to our mailing list
instead of opening an issue:
https://groups.google.com/forum/#!forum/netbox-discuss
NOTE: Due to an excessive backlog of feature requests, we are not currently
accepting any proposals which significantly extend NetBox's feature scope.
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.3.6 -->
<!--
Describe in detail the new functionality you are proposing. Include any
specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
<!--
Convey an example use case for your proposed feature. Write from the
perspective of a NetBox user who would benefit from the proposed
functionality and describe how.
--->
### Use Case
<!--
Note any changes to the database schema necessary to support the new
feature. For example, does the proposal require adding a new model or
field? (Not all new features require database changes.)
--->
### Database Changes
<!--
List any new dependencies on external libraries or services that this new
feature would introduce. For example, does the proposal require the
installation of a new Python package? (Not all new features introduce new
dependencies.)
-->
### External Dependencies

16
.github/ISSUE_TEMPLATE/housekeeping.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: :house: Housekeeping
about: A change pertaining to the codebase itself
---
<!--
NOTE: This type of issue should be opened only by those reasonably familiar
with NetBox's code base and interested in contributing to its development.
Describe the proposed change(s) in detail.
-->
### Proposed Changes
<!-- Provide justification for the proposed change(s). -->
### Justification -->

View File

@@ -6,6 +6,8 @@
be able to accept.
Please indicate the relevant feature request or bug report below.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
-->
### Fixes:

View File

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

View File

@@ -91,11 +91,13 @@ appropriate labels will be applied for categorization.
## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and discuss
your idea with the NetBox maintainers before beginning work. This will help
prevent wasting time on something that might we might not be able to implement.
When suggesting a new feature, also make sure it won't conflict with any work
that's already in progress.
* Be sure to open an issue **before** starting work on a pull request, and
discuss your idea with the NetBox maintainers before beginning work. This will
help prevent wasting time on something that might we might not be able to
implement. When suggesting a new feature, also make sure it won't conflict with
any work that's already in progress.
* Any pull request which does _not_ relate to an accepted issue will be closed.
* When submitting a pull request, please be sure to work off of the `develop`
branch, rather than `master`. The `develop` branch is used for ongoing

View File

@@ -206,3 +206,28 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_
!!! warning
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
# Filtering
A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`):
```
GET /api/ipam/prefixes/?status=1
```
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
```
GET /api/ipam/prefixes/?status=1&status=2
```
## Custom Fields
To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
```
GET /api/dcim/sites/?cf_foo=123
```
!!! note
Full versus partial matching when filtering is configurable per custom field. Filtering can be toggled (or disabled) for a custom field in the admin UI.

View File

@@ -42,6 +42,8 @@ A device type represents a particular hardware model that exists in the real wor
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type.
A device type can be a parent, child, or neither. Parent devices house child devices in device bays. This relationship is used to model things like blade servers, where child devices function independently but share physical resources like rack space and power. Note that this is **not** intended to model chassis-based devices, wherein child members share a common control plane.
### Manufacturers
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.

View File

@@ -81,7 +81,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
# User Groups for Permissions
!!! info
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType

View File

@@ -32,7 +32,7 @@ class DeviceIPsReport(Report):
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
```
from dcim.constants import CONNECTION_STATUS_PLANNED, STATUS_ACTIVE
from dcim.constants import CONNECTION_STATUS_PLANNED, DEVICE_STATUS_ACTIVE
from dcim.models import ConsolePort, Device, PowerPort
from extras.reports import Report
@@ -43,7 +43,7 @@ class DeviceConnectionsReport(Report):
def test_console_connection(self):
# Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.select_related('device').filter(device__status=STATUS_ACTIVE):
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
if console_port.cs_port is None:
self.log_failure(
console_port.device,
@@ -60,7 +60,7 @@ class DeviceConnectionsReport(Report):
def test_power_connections(self):
# Check that every active device has at least two connected power supplies.
for device in Device.objects.filter(status=STATUS_ACTIVE):
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.power_outlet is not None:

View File

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

View File

@@ -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']),
)
@@ -267,7 +267,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
import napalm
except ImportError:
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
from napalm.base.exceptions import ModuleImportError
# Validate the configured driver
try:
@@ -281,16 +281,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
if not request.user.has_perm('dcim.napalm_read'):
return HttpResponseForbidden()
# Validate requested NAPALM methods
# Connect to the device
napalm_methods = request.GET.getlist('method')
for method in napalm_methods:
if not hasattr(driver, method):
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
elif not method.startswith('get_'):
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
# Connect to the device and execute the requested methods
# TODO: Improve error handling
response = OrderedDict([(m, None) for m in napalm_methods])
ip_address = str(device.primary_ip.address.ip)
d = driver(
@@ -302,12 +294,23 @@ class DeviceViewSet(CustomFieldModelViewSet):
)
try:
d.open()
for method in napalm_methods:
response[method] = getattr(d, method)()
except Exception as e:
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
# Validate and execute each specified NAPALM method
for method in napalm_methods:
if not hasattr(driver, method):
response[method] = {'error': 'Unknown NAPALM method'}
continue
if not method.startswith('get_'):
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue
try:
response[method] = getattr(d, method)()
except NotImplementedError:
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
d.close()
return Response(response)

View File

@@ -509,7 +509,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(name__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(inventory_items__serial__icontains=value.strip()) |
Q(asset_tag=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
).distinct()

View File

@@ -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 />
@@ -1780,8 +1780,9 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
if parent is not None:
# Add site VLANs
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
if parent.site:
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=parent.site):

View File

@@ -781,9 +781,6 @@ class DeviceRole(models.Model):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
def to_csv(self):
return (
self.name,
@@ -963,6 +960,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 +1208,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')
@@ -1287,8 +1290,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')

View File

@@ -11,13 +11,8 @@ 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=vc_position)
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
@receiver(pre_delete, sender=VirtualChassis)

View File

@@ -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 = """
@@ -408,7 +408,6 @@ class DeviceBayTemplateTable(BaseTable):
class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.TemplateColumn(
template_code=DEVICEROLE_DEVICE_COUNT,
accessor=Accessor('devices.count'),
@@ -594,7 +593,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')

View File

@@ -2075,7 +2075,7 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class VirtualChassisListView(ObjectListView):
queryset = VirtualChassis.objects.annotate(member_count=Count('members'))
queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
table = tables.VirtualChassisTable
filter = filters.VirtualChassisFilter
filter_form = forms.VirtualChassisFilterForm

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,10 @@ class PrefixViewSet(CustomFieldModelViewSet):
)
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
available_ips = iter(available_ips)
prefix_length = prefix.prefix.prefixlen
for requested_ip in requested_ips:
requested_ip['address'] = available_ips.pop(0)
requested_ip['address'] = '{}/{}'.format(next(available_ips), prefix_length)
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

View File

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

View File

@@ -194,17 +194,35 @@ class RIRTable(BaseTable):
class RIRDetailTable(RIRTable):
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data))
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
footer=lambda table: sum(r.stats['active'] for r in table.data))
stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
footer=lambda table: sum(r.stats['reserved'] for r in table.data))
stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data))
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
stats_total = tables.Column(
accessor='stats.total',
verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data)
)
stats_active = tables.Column(
accessor='stats.active',
verbose_name='Active',
footer=lambda table: sum(r.stats['active'] for r in table.data)
)
stats_reserved = tables.Column(
accessor='stats.reserved',
verbose_name='Reserved',
footer=lambda table: sum(r.stats['reserved'] for r in table.data)
)
stats_deprecated = tables.Column(
accessor='stats.deprecated',
verbose_name='Deprecated',
footer=lambda table: sum(r.stats['deprecated'] for r in table.data)
)
stats_available = tables.Column(
accessor='stats.available',
verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data)
)
utilization = tables.TemplateColumn(
template_code=RIR_UTILIZATION,
verbose_name='Utilization'
)
class Meta(RIRTable.Meta):
fields = (

View File

@@ -192,9 +192,15 @@ class RIRListView(ObjectListView):
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
# Find all consumed space for each prefix status (we ignore containers for this purpose).
active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
active_prefixes = netaddr.cidr_merge(
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
)
reserved_prefixes = netaddr.cidr_merge(
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
)
deprecated_prefixes = netaddr.cidr_merge(
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
)
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
available_prefixes = (
@@ -205,11 +211,11 @@ class RIRListView(ObjectListView):
)
# Add the size of each metric to the RIR total.
stats['total'] += aggregate.prefix.size / denominator
stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
stats['available'] += available_prefixes.size / denominator
stats['total'] += int(aggregate.prefix.size / denominator)
stats['active'] += int(netaddr.IPSet(active_prefixes).size / denominator)
stats['reserved'] += int(netaddr.IPSet(reserved_prefixes).size / denominator)
stats['deprecated'] += int(netaddr.IPSet(deprecated_prefixes).size / denominator)
stats['available'] += int(available_prefixes.size / denominator)
# Calculate the percentage of total space for each prefix status.
total = float(stats['total'])
@@ -229,20 +235,6 @@ class RIRListView(ObjectListView):
return rirs
def extra_context(self):
totals = {
'total': sum([rir.stats['total'] for rir in self.queryset]),
'active': sum([rir.stats['active'] for rir in self.queryset]),
'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
'available': sum([rir.stats['available'] for rir in self.queryset]),
}
return {
'totals': totals,
}
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir'

View File

@@ -15,6 +15,7 @@ OBJ_TYPE_CHOICES = (
('rack', 'Racks'),
('devicetype', 'Device types'),
('device', 'Devices'),
('virtualchassis', 'Virtual Chassis'),
)),
('IPAM', (
('vrf', 'VRFs'),

View File

@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning
)
VERSION = '2.3.4'
VERSION = '2.3.7'
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'

View File

@@ -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}),
@@ -74,3 +74,5 @@ if settings.DEBUG:
urlpatterns = [
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
]
handler500 = 'utilities.views.server_error'

View File

@@ -12,9 +12,9 @@ from rest_framework.views import APIView
from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider
from circuits.tables import CircuitTable, ProviderTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter, VirtualChassisFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site, VirtualChassis
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable, VirtualChassisTable
from extras.models import ReportResult, TopologyMap, UserAction
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
@@ -72,6 +72,12 @@ SEARCH_TYPES = OrderedDict((
'table': DeviceDetailTable,
'url': 'dcim:device_list',
}),
('virtualchassis', {
'queryset': VirtualChassis.objects.select_related('master').annotate(member_count=Count('members')),
'filter': VirtualChassisFilter,
'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
# IPAM
('vrf', {
'queryset': VRF.objects.select_related('tenant'),

View File

@@ -372,12 +372,19 @@ table.reports td.method {
font-family: monospace;
padding-left: 30px;
}
table.reports td.stats label {
td.report-stats label {
display: inline-block;
line-height: 14px;
margin-bottom: 0;
min-width: 40px;
}
table.report th {
position: relative;
}
table.report th a {
position: absolute;
top: -51px;
}
/* AJAX loader */
.loading {

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.")
try:
PKCS1_OAEP.new(key)
except:
except Exception:
raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.")
@@ -153,7 +153,8 @@ class UserKeyForm(BootstrapMixin, forms.ModelForm):
model = UserKey
fields = ['public_key']
help_texts = {
'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption.",
'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. "
"Please note that passphrase-protected keys are not supported.",
}
def clean_public_key(self):

View File

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

View File

@@ -146,7 +146,7 @@
<tr>
<td>Role</td>
<td>
<a href="{{ device.device_role.get_absolute_url }}">{{ device.device_role }}</a>
<a href="{% url 'dcim:device_list' %}?role={{ device.device_role.slug }}">{{ device.device_role }}</a>
</td>
</tr>
<tr>
@@ -387,6 +387,7 @@
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Status</th>
<th colspan="2">Installed Device</th>
<th></th>
</tr>

View File

@@ -8,6 +8,9 @@
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
</td>
{% if devicebay.installed_device %}
<td>
<span class="label label-{{ devicebay.installed_device.get_status_class }}">{{ devicebay.installed_device.get_status_display }}</span>
</td>
<td>
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
</td>
@@ -15,6 +18,7 @@
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
</td>
{% else %}
<td></td>
<td colspan="2">
<span class="text-muted">Vacant</span>
</td>

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 %}>
{% 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 }})

View File

@@ -29,63 +29,73 @@
<p class="lead">{{ report.description }}</p>
{% endif %}
{% if report.result %}
<p>Last run: {{ report.result.created }}</p>
{% else %}
<p class="text-muted">Last run: Never</p>
<p>Last run: <strong>{{ report.result.created }}</strong></p>
{% endif %}
</div>
<div class="col-md-9">
{% if report.result %}
<table class="table table-hover">
<thead>
<tr>
<th>Time</th>
<th>Level</th>
<th>Object</th>
<th>Message</th>
</tr>
</thead>
{% for method, data in report.result.data.items %}
<tr>
<th colspan="4"><a name="{{ method }}"></a>{{ method }}</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Report Methods</strong>
</div>
<table class="table table-hover panel-body">
{% for method, data in report.result.data.items %}
<tr>
<td><code><a href="#{{ method }}">{{ method }}</a></code></td>
<td class="text-right report-stats">
<label class="label label-success">{{ data.success }}</label>
<label class="label label-info">{{ data.info }}</label>
<label class="label label-warning">{{ data.warning }}</label>
<label class="label label-danger">{{ data.failure }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% endif %}
</td>
<td>{{ message }}</td>
</tr>
{% endfor %}
{% endfor %}
</table>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Report Results</strong>
</div>
<table class="table table-hover panel-body report">
<thead>
<tr class="table-headings">
<th>Time</th>
<th>Level</th>
<th>Object</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for method, data in report.result.data.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ method }}"></a>{{ method }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% endif %}
</td>
<td>{{ message }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="well">No results are available for this report. Please run the report first.</div>
{% endif %}
</div>
<div class="col-md-3">
{% if report.result %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Methods</strong>
</div>
<ul class="list-group">
{% for method, data in report.result.data.items %}
<li class="list-group-item">
<a href="#{{ method }}">{{ method }}</a>
<span class="badge">{{ data.log|length }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>

View File

@@ -38,7 +38,7 @@
<td colspan="3" class="method">
{{ method }}
</td>
<td class="text-right stats">
<td class="text-right report-stats">
<label class="label label-success">{{ stats.success }}</label>
<label class="label label-info">{{ stats.info }}</label>
<label class="label label-warning">{{ stats.warning }}</label>
@@ -69,7 +69,7 @@
<a href="#report.{{ report.name }}" class="list-group-item">
<i class="fa fa-list-alt"></i> {{ report.name }}
<div class="pull-right">
{% include 'extras/inc/report_label.html' %}
{% include 'extras/inc/report_label.html' with result=report.result %}
</div>
</a>
{% endfor %}

View File

@@ -65,10 +65,11 @@
<td>Tenant</td>
<td>
{% if ipaddress.tenant %}
{% if ipaddress.tenant.group %}
<a href="{{ ipaddress.tenant.group.get_absolute_url }}">{{ ipaddress.tenant.group }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
{% elif ipaddress.vrf.tenant %}
<a href="{{ ipaddress.vrf.tenant.get_absolute_url }}">{{ ipaddress.vrf.tenant }}</a>
<label class="label label-info">Inherited</label>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@@ -35,13 +35,6 @@
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %}
{% if prefix.vrf.tenant.group %}
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
<label class="label label-info">Inherited</label>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@@ -78,14 +78,8 @@ class TenantView(View):
'rackreservation_count': RackReservation.objects.filter(tenant=tenant).count(),
'device_count': Device.objects.filter(tenant=tenant).count(),
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
'prefix_count': Prefix.objects.filter(
Q(tenant=tenant) |
Q(tenant__isnull=True, vrf__tenant=tenant)
).count(),
'ipaddress_count': IPAddress.objects.filter(
Q(tenant=tenant) |
Q(tenant__isnull=True, vrf__tenant=tenant)
).count(),
'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(),

View File

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

View File

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

View File

@@ -5,9 +5,10 @@ import sys
from django.conf import settings
from django.db import ProgrammingError
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from .views import server_error
BASE_PATH = getattr(settings, 'BASE_PATH', False)
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
@@ -65,23 +66,19 @@ class ExceptionHandlingMiddleware(object):
if isinstance(exception, Http404):
return
# Determine the type of exception
# Determine the type of exception. If it's a common issue, return a custom error page with instructions.
custom_template = None
if isinstance(exception, ProgrammingError):
template_name = 'exceptions/programming_error.html'
custom_template = 'exceptions/programming_error.html'
elif isinstance(exception, ImportError):
template_name = 'exceptions/import_error.html'
custom_template = 'exceptions/import_error.html'
elif (
sys.version_info[0] >= 3 and isinstance(exception, PermissionError)
) or (
isinstance(exception, OSError) and exception.errno == 13
):
template_name = 'exceptions/permission_error.html'
else:
template_name = '500.html'
custom_template = 'exceptions/permission_error.html'
# Return an error message
type_, error, traceback = sys.exc_info()
return render(request, template_name, {
'exception': str(type_),
'error': error,
}, status=500)
# Return a custom error message, or fall back to Django's default 500 error handling
if custom_template:
return server_error(request, template_name=custom_template)

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

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from collections import OrderedDict
from copy import deepcopy
import sys
from django.conf import settings
from django.contrib import messages
@@ -10,12 +11,16 @@ from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.http import HttpResponseServerError
from django.shortcuts import get_object_or_404, redirect, render
from django.template.exceptions import TemplateSyntaxError
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import ERROR_500_TEMPLATE_NAME
from django.views.generic import View
from django_tables2 import RequestConfig
@@ -858,3 +863,20 @@ class BulkComponentCreateView(View):
'table': table,
'return_url': reverse(self.default_return_url),
})
@requires_csrf_token
def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
"""
Custom 500 handler to provide additional context when rendering 500.html.
"""
try:
template = loader.get_template(template_name)
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
type_, error, traceback = sys.exc_info()
return HttpResponseServerError(template.render({
'exception': str(type_),
'error': error,
}))

View File

@@ -1,7 +1,7 @@
Django>=1.11,<2.0
django-cors-headers>=2.1.0
django-debug-toolbar>=1.9.0
django-filter>=1.1.0
django-filter==1.1.0
django-mptt>=0.9.0
django-tables2>=1.19.0
django-timezone-field>=2.0

View File

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