mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-29 08:37:46 -06:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a85e6370a8 | ||
|
|
0497539ef2 | ||
|
|
431361efad | ||
|
|
e82bf66a76 | ||
|
|
c8a73b5b15 | ||
|
|
b518258e6d | ||
|
|
a1d45023ab | ||
|
|
ba3ae0d80a | ||
|
|
d04727f4b5 | ||
|
|
93ce0ce670 | ||
|
|
c2573774bf | ||
|
|
6e037e91d3 | ||
|
|
d665d4d62a | ||
|
|
29d9b32b67 | ||
|
|
00d218118c | ||
|
|
02b6ffd59a | ||
|
|
aa0e4406eb | ||
|
|
786f389be8 | ||
|
|
09a03565d7 | ||
|
|
456b058462 | ||
|
|
ecaba5b32e | ||
|
|
9f4c77d6d7 | ||
|
|
1fb67b791f | ||
|
|
a26d1812c2 | ||
|
|
b6e354085e | ||
|
|
108e9722fa | ||
|
|
72cb1cbfff | ||
|
|
ed84c4b210 | ||
|
|
77518eaf69 | ||
|
|
4bd36f0ea9 | ||
|
|
b19bf791a4 | ||
|
|
f70b7cab21 | ||
|
|
b10635a9b1 | ||
|
|
302c14186a | ||
|
|
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 | ||
|
|
82189de78e | ||
|
|
8bad3aee74 | ||
|
|
acc59a9da5 | ||
|
|
03ce4bdfca |
48
.github/ISSUE_TEMPLATE.md
vendored
48
.github/ISSUE_TEMPLATE.md
vendored
@@ -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
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
|
||||
17
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal 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
|
||||
53
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
53
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
16
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
Normal 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 -->
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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:
|
||||
|
||||
|
||||
@@ -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();'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,6 +19,7 @@ from . import serializers
|
||||
|
||||
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Circuit, ['status']),
|
||||
(CircuitTermination, ['term_side']),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -15,6 +15,7 @@ OBJ_TYPE_CHOICES = (
|
||||
('rack', 'Racks'),
|
||||
('devicetype', 'Device types'),
|
||||
('device', 'Devices'),
|
||||
('virtualchassis', 'Virtual Chassis'),
|
||||
)),
|
||||
('IPAM', (
|
||||
('vrf', 'VRFs'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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).")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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