Compare commits

..

104 Commits

Author SHA1 Message Date
Jeremy Stretch
106f0ad446 Release v2.2-beta1 2017-09-14 15:01:07 -04:00
Jeremy Stretch
3466da4338 Merge branch 'develop' into develop-2.2
Conflicts:
	netbox/dcim/forms.py
2017-09-14 14:58:45 -04:00
Jeremy Stretch
17c1a1e465 Use add_blank_choice() to prepend a null choice to field options 2017-09-14 14:45:45 -04:00
Jeremy Stretch
6872ab0e14 Added a status field for virtual machines 2017-09-14 14:35:34 -04:00
Jeremy Stretch
136d16b7fd Don't add a placeholder option to multiselect widgets 2017-09-12 14:14:08 -04:00
Jeremy Stretch
d651deb31c Renamed form_class attribute to model_form for consistency 2017-09-12 13:55:11 -04:00
Jeremy Stretch
8fb37233f4 Started documenting utility views 2017-09-12 13:53:15 -04:00
Jeremy Stretch
700194b80d Implemented bulk interface creation for virtual machines 2017-09-12 12:49:01 -04:00
Jeremy Stretch
ef2dd673ec Finished bulk edit/delete views 2017-09-11 16:14:05 -04:00
Jeremy Stretch
7a3adca771 Added device count to clusters table 2017-09-11 15:51:03 -04:00
Jeremy Stretch
f66b0b7fea Finished implementing CSV imports 2017-09-11 15:42:18 -04:00
huzichunjohn
b6df0209ba Fixes #1472: Secret truncated when using '<' character (#1477) 2017-09-11 10:55:04 -04:00
Jeremy Stretch
6b53d263fe Fixes #1469: Allow a NAT IP to be assigned as the primary IP for a device 2017-09-01 13:00:44 -04:00
Jeremy Stretch
00024240bb Merge branch 'virtualization' into develop-2.2 2017-09-01 12:29:12 -04:00
Jeremy Stretch
237a889f54 Preserve query parameters on redirect after clicking "add another" 2017-09-01 12:28:34 -04:00
Jeremy Stretch
f5c265b7b5 Modified form action to preserve query parameters 2017-09-01 12:25:57 -04:00
Jeremy Stretch
1341ab5703 Fixed selection of sites not assigned to a region 2017-09-01 10:10:10 -04:00
Jeremy Stretch
3ded8196c4 Fixed IP toggle button 2017-09-01 10:09:52 -04:00
Jeremy Stretch
cbc239ceaa Added virtualization API tests 2017-08-31 14:03:12 -04:00
Jeremy Stretch
a6b43baafe Added documentation for virtualization data model 2017-08-31 13:22:12 -04:00
Jeremy Stretch
ba0a261840 Removed extraneous import 2017-08-31 13:04:56 -04:00
Jeremy Stretch
3bb0d523d3 Enabled services on virtual machines 2017-08-31 12:50:35 -04:00
Jeremy Stretch
85cb333a5d Added virtualization to API root 2017-08-31 11:36:29 -04:00
Jeremy Stretch
2abb073b3a Regenerated migrations 2017-08-31 10:17:59 -04:00
Jeremy Stretch
0634386b2e Merge branch 'develop' into virtualization 2017-08-31 10:14:50 -04:00
Jeremy Stretch
372e9335b1 Post-release version bump 2017-08-30 14:44:55 -04:00
Jeremy Stretch
e6b6082a2b Release v2.1.4 2017-08-30 14:40:52 -04:00
Jeremy Stretch
e0ee0b9254 Closes #1460: Hostnames with no domain are now acceptable in custom URL fields 2017-08-30 14:08:39 -04:00
Jeremy Stretch
fde47133da Fix global search placeholder text 2017-08-30 12:07:11 -04:00
Jeremy Stretch
643f64df3f Closes #1341: Added a MEDIA_ROOT configuration setting to specify where uploaded files are stored on disk 2017-08-30 11:11:59 -04:00
Jeremy Stretch
a6ec1ba23b Closes #1376: Ignore anycast when detecting duplicate IPs 2017-08-30 10:46:30 -04:00
Jeremy Stretch
ae231b1d1b Moved circuit speed humanization to a template tag 2017-08-29 22:42:06 -04:00
Jeremy Stretch
ecdf66c454 Closes #1326: Added dropdown widget with common values for circuit speed fields 2017-08-29 22:29:30 -04:00
Jeremy Stretch
aaca28c1a6 Closes #1402: Increased max length of name field for device components 2017-08-29 17:27:30 -04:00
Jeremy Stretch
f0d8fdf2d8 Closes #1431: Added interface form factor for 10GBASE-CX4 2017-08-29 17:02:09 -04:00
Jeremy Stretch
c9288a052a Closes #1432: Added a commit_rate field to the circuits list search form 2017-08-29 16:48:11 -04:00
Jeremy Stretch
4b3a425888 Fixed role link on IP address view 2017-08-29 16:14:32 -04:00
Jeremy Stretch
71069ced7f Added platform info to VM view 2017-08-29 16:12:52 -04:00
Jeremy Stretch
1308839a41 Fixes #1458: Corrected permission name on prefix/VLAN roles list 2017-08-29 15:36:51 -04:00
Jeremy Stretch
6d849ad5f6 Deleted obsolete templates 2017-08-29 15:31:11 -04:00
Jeremy Stretch
fa95191792 Allowed assigning an IP address to either a device or a VM 2017-08-29 15:26:35 -04:00
Jeremy Stretch
e945aafd7b Added fitlers to return the verbose model name of an object 2017-08-29 15:10:52 -04:00
Jeremy Stretch
e9f75adddb Ditched VMInterface in favor of reusing dcim.Interface 2017-08-29 14:24:58 -04:00
Jeremy Stretch
3e4b66675b Rearranged circuits navigation dropdown 2017-08-24 12:51:50 -04:00
Jeremy Stretch
6f8d3f7a57 Fixes #1443: Fixed API validation error involving custom field data 2017-08-22 11:31:33 -04:00
Jeremy Stretch
eb994a691a Added style to BaseTable's empty_text 2017-08-21 17:38:27 -04:00
Jeremy Stretch
6ebd8e78c8 Added VMs to tenant view 2017-08-21 17:21:02 -04:00
Jeremy Stretch
daa947e7eb Added virtualization models to home/search 2017-08-21 17:17:01 -04:00
Jeremy Stretch
b117130aac Added icons 2017-08-21 17:02:35 -04:00
Jeremy Stretch
4587aba1d4 Added views to add/remove hosts to/from clusters 2017-08-21 16:53:36 -04:00
Jeremy Stretch
ae73e6f04d Removed task list formatting 2017-08-21 11:10:07 -04:00
Jeremy Stretch
5930a64203 Converted IPAddress.interface to a GenericForeignKey 2017-08-18 16:57:20 -04:00
Jeremy Stretch
97536c4e9b Genericized component edit/delete views 2017-08-18 14:43:11 -04:00
Jeremy Stretch
e81e33af38 Added views for VM interfaces 2017-08-18 14:37:19 -04:00
Jeremy Stretch
a7c56eab86 Moved DeviceComponentForm to utilities.forms 2017-08-18 13:10:19 -04:00
Jeremy Stretch
6e580a731e Added template names to device component create views 2017-08-18 12:29:39 -04:00
Jeremy Stretch
7843a6bf77 Fixed devicetype_id filter for DeviceType components 2017-08-18 10:27:43 -04:00
Jeremy Stretch
fa6d933544 Converted list of issue types to a task list 2017-08-17 15:14:20 -04:00
Jeremy Stretch
83085d08ca Revised the GitHub issue template 2017-08-17 15:11:55 -04:00
Jeremy Stretch
a02055e9b5 Reworked relationship between devices and clusters 2017-08-16 17:21:34 -04:00
Jeremy Stretch
9acd792abe Added virtualization filters 2017-08-16 17:00:17 -04:00
Jeremy Stretch
4ef55502b4 Updates for v2.1.3 and expanded VirtualMachine model 2017-08-16 15:25:33 -04:00
Jeremy Stretch
3009863877 Merge branch 'develop' into virtualization 2017-08-16 15:08:37 -04:00
Jeremy Stretch
70a6b171d0 Fixes #1429: Fixed uptime formatting on device status page 2017-08-16 11:20:52 -04:00
Jeremy Stretch
9a25b5458d Merge pull request #1430 from xmudrii/fix-docs
docs: web-server link to new ssl tutorials for 16.04
2017-08-15 21:39:59 -04:00
Marko Mudrinić
416aeb4072 docs: web-server link to new ssl tutorials for 16.04 2017-08-15 23:39:10 +02:00
Jeremy Stretch
0c1594b8f8 Post-release version bump 2017-08-15 15:53:10 -04:00
Jeremy Stretch
669ae104a4 Release v2.1.3 2017-08-15 15:50:51 -04:00
Jeremy Stretch
c37cfeb74f Fixed page titles in the browsable API 2017-08-15 15:30:45 -04:00
Jeremy Stretch
51b1da660a Fixes #1330: Raise validation error when assigning an unrelated IP as the primary IP for a device 2017-08-15 14:05:02 -04:00
Jeremy Stretch
c394985b1b Fixes #1421: Improved model validation logic for API serializers 2017-08-15 13:54:04 -04:00
Jeremy Stretch
04c300b8e2 Fixes #1420: Exclude virtual interfaces from device LLDP neighbors view 2017-08-15 11:07:26 -04:00
Jeremy Stretch
117f33afc5 Fixes #1419: Allow editing image attachments without re-uploading an image 2017-08-11 10:47:06 -04:00
Jeremy Stretch
3d92669df5 Corrected HTTP verb in API docs 2017-08-10 11:41:25 -04:00
Jeremy Stretch
63757af1a0 Expanded API overview documentation 2017-08-09 16:55:49 -04:00
Jeremy Stretch
e6a58b6700 Fixes #1415: Ignore leading/trailing semicolons in topology map device lists 2017-08-09 15:46:40 -04:00
Jeremy Stretch
8fb504c963 Tweaked installation docs to better accommodate Python 3 2017-08-09 14:19:33 -04:00
Jeremy Stretch
babe42ef35 Closes #1414: Selecting a site from the rack filters automatically updates the available rack groups 2017-08-09 13:59:25 -04:00
Jeremy Stretch
b67e3ff957 Implemented initial virtualization API 2017-08-08 16:33:34 -04:00
Jeremy Stretch
7557220d5d Fixes #1389: Avoid splitting carat/prefix on prefix list 2017-08-08 11:48:51 -04:00
Jeremy Stretch
50ccccc3f4 Merge pull request #1403 from huzichunjohn/issue_1400
Fixes #1400: Device interface shows twice on IP Addresses page
2017-08-08 08:23:14 -04:00
johnhu
b6afc68847 Fixes #1400: Device interface shows twice on IP Addresses page 2017-08-08 11:44:50 +00:00
Jeremy Stretch
d06813f528 Initial work on virtualization support (#142) 2017-08-04 17:02:52 -04:00
Jeremy Stretch
36d5debe74 Post-release version bump 2017-08-04 10:52:29 -04:00
Jeremy Stretch
a7d5fb5006 Release v2.1.2 2017-08-04 10:52:06 -04:00
Jeremy Stretch
dadfcd4f81 Tweaked navigation menu styling 2017-08-03 17:49:22 -04:00
Jeremy Stretch
236229ea7e Closes #992: Allow the creation of multiple services per device with the same protocol and port 2017-08-03 15:40:52 -04:00
Jeremy Stretch
6f0f3cc115 Fixes #1390: Fixed IndexError when viewing available IPs within large IPv6 prefixes 2017-08-03 10:09:37 -04:00
Jeremy Stretch
3aa072b437 Fixes #1388: Fixed server error when searching globally for IPs/prefixes (rolled back #1379) 2017-08-03 09:26:28 -04:00
Jeremy Stretch
930b15ae09 Post-release version bump 2017-08-02 14:20:43 -04:00
Jeremy Stretch
1c1f4068cd Release v2.1.1 2017-08-02 14:20:20 -04:00
Jeremy Stretch
b6690e0012 Closes #1368: Render reservations in rack elevations view 2017-08-02 13:33:44 -04:00
Jeremy Stretch
52e7f670f3 Deleted obsolete template 2017-08-02 13:17:34 -04:00
Jeremy Stretch
12d643694f Cleaned up title blocks 2017-08-02 13:15:28 -04:00
Jeremy Stretch
3876a96b67 Tweaked IP address layout 2017-08-02 12:55:21 -04:00
Jeremy Stretch
e50f234ba3 Closes #1379: Allow searching devices by interface MAC address in global search 2017-08-02 11:54:53 -04:00
Jeremy Stretch
fd6df8e52a Fixes #1385: Connected device API endpoint no longer requires authentication if LOGIN_REQUIRED=False 2017-08-02 11:17:57 -04:00
Jeremy Stretch
28225c7cbd Merge pull request #1386 from bdlamprecht/patch-1
Updated ldap.md
2017-08-02 10:56:41 -04:00
bdlamprecht
cab2929c10 Updated ldap.md
Add mapping of `email` field to `mail` value in LDAP.  Also remove duplicate entries of such mapping information.
2017-08-02 08:45:00 -06:00
Jeremy Stretch
52a490bf5d Fixes #461: Display a validation error when attempting to assigning a new child device to a rack face/position 2017-07-28 10:05:51 -04:00
Jeremy Stretch
f2c16fbf3c Closes #893: Allow filtering by null values for NullCharacterFields (e.g. return only unnamed devices) 2017-07-27 09:53:41 -04:00
Jeremy Stretch
65a633f42d Closes #1374: Added NAPALM_ARGS and NAPALM_TIMEOUT configiuration parameters 2017-07-26 11:47:59 -04:00
Jeremy Stretch
d2c6d79589 Closes #1375: Renamed NETBOX_USERNAME and NETBOX_PASSWORD configuration parameters to NAPALM_USERNAME and NAPALM_PASSWORD 2017-07-26 11:24:19 -04:00
Jeremy Stretch
f4a873745f Post-release version bump 2017-07-25 11:26:50 -04:00
160 changed files with 4242 additions and 990 deletions

View File

@@ -1,28 +1,41 @@
<!--
Please note: GitHub issues are to be used only for feature requests
and bug reports. For installation assistance or general discussion,
please join us on the mailing list:
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. If none of the below apply, please raise your issue for
discussion on our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please indicate "bug report" or "feature request" below. Be sure to
search the existing set of issues (both open and closed) to see if
a similar issue has already been raised.
-->
### Issue type:
Please note that issues which do not fall under any of the below categories
will be closed.
--->
### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature -->
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
[ ] Documentation <!-- Proposing a modification to the documentation -->
<!--
If filing a bug, please indicate the version of Python and NetBox
you are running. (This is not necessary for feature requests.)
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.)
-->
**Python version:**
**NetBox version:**
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!--
If filing a bug, please record the exact steps taken to reproduce
the bug and any errors messages that are generated.
BUG REPORTS must include:
* A list of the steps needed to reproduce the bug
* A description of the expected behavior
* Any relevant error messages (screenshots may also help)
If filing a feature request, please precisely describe the data
model or workflow you would like to see implemented, and provide a
use case.
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

View File

@@ -1,8 +1,42 @@
NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally.
# What is a REST API?
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb:
* `GET`: Retrieve an object or list of objects
* `POST`: Create an object
* `PUT` / `PATCH`: Modify an existing object
* `DELETE`: Delete an existing object
The NetBox API represents all objects in [JavaScript Object Notation (JSON)](http://www.json.org/). This makes it very easy to interact with NetBox data on the command line with common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)
```
$ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.'
{
"custom_fields": {},
"nat_outside": null,
"nat_inside": null,
"description": "An example IP address",
"id": 2954,
"family": 4,
"address": "5.101.108.132/26",
"vrf": null,
"tenant": null,
"status": {
"label": "Active",
"value": 1
},
"role": null,
"interface": null
}
```
Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.
# URL Hierarchy
NetBox's entire REST API is housed under the API root, `/api/`. The API's URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
NetBox's entire API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
* /api/circuits/providers/
* /api/circuits/circuits/
@@ -13,9 +47,9 @@ Likewise, the site, rack, and device objects are located under the "DCIM" applic
* /api/dcim/racks/
* /api/dcim/devices/
The full hierarchy of available endpoints can be viewed by navigating to the API root (e.g. /api/) in a web browser.
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.
Each model generally has two URLs associated with it: a list URL and a detail URL. The list URL is used to request a list of multiple objects or to create a new object. The detail URL is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (ID).
Each model generally has two views associated with it: a list view and a detail view. The list view is used to request a list of multiple objects or to create a new object. The detail view is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (`id`).
* /api/dcim/devices/ - List devices or create a new device
* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123

View File

@@ -135,11 +135,61 @@ An API consumer can request an arbitrary number of objects by appending the "lim
---
## NETBOX_USERNAME
## MEDIA_ROOT
## NETBOX_PASSWORD
Default: $BASE_DIR/netbox/media/
If provided, NetBox will use these credentials to authenticate against devices when collecting data.
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media` directory within the base NetBox installation path.
---
## NAPALM_USERNAME
## NAPALM_PASSWORD
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed.
---
## NAPALM_ARGS
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
```
NAPALM_ARGS = {
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
'port': 2222,
}
```
Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
```
NAPALM_USERNAME = 'username'
NAPALM_PASSWORD = 'MySecretPassword'
NAPALM_ARGS = {
'secret': NAPALM_PASSWORD,
# Include any additional args here
}
```
---
## NAPALM_TIMEOUT
Default: 30 seconds
The amount of time (in seconds) to wait for NAPALM to connect to a device.
---
## NETBOX_USERNAME (Deprecated)
## NETBOX_PASSWORD (Deprecated)
These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead.
---

View File

@@ -96,4 +96,4 @@ VLAN groups can be employed for administrative organization within NetBox. Each
# Services
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
A service represents a TCP or UDP service available on a device or virtual machine. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to its parent. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)

View File

@@ -0,0 +1,29 @@
NetBox supports the definition of virtual machines arranged in clusters. A cluster can optionally have physical host devices associated with it.
# Clusters
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned an organizational group.
Physical devices (from NetBox's DCIM component) may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
### Cluster Types
A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.
### Cluster Groups
Cluster groups may be created for the purpose of organizing clusters.
---
# Virtual Machines
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
Like devices, each VM can have interfaces created on it. These behave similarly to device interfaces, and can be assigned IP addresses, however given their virtual nature they cannot be connected to other interfaces. VMs can also be assigned layer four services. Unlike physical devices, VMs cannot be assigned console or power ports, or device bays.
The following resources can be defined for each VM:
* vCPU count
* Memory (MB)
* Disk space (GB)

View File

@@ -0,0 +1,53 @@
# Utility Views
Utility views are reusable views that handle common CRUD tasks, such as listing and updating objects. Some views operate on individual objects, whereas others (referred to as "bulk" views) operate on multiple objects at once.
## Individual Views
### ObjectListView
Generates a paginated table of objects from a given queryset, which may optionally be filtered.
### ObjectEditView
Updates an object identified by a primary key (PK) or slug. If no existing object is specified, a new object will be created.
### ObjectDeleteView
Deletes an object. The user is redirected to a confirmation page before the deletion is executed.
## Bulk Views
### BulkCreateView
Creates multiple objects at once based on a given pattern. Currently used only for IP addresses.
### BulkImportView
Accepts CSV-formatted data and creates a new object for each line. Creation is all-or-none.
### BulkEditView
Applies changes to multiple objects at once in a two-step operation. First, the list of PKs for selected objects is POSTed and an edit form is presented to the user. On submission of that form, the specified changes are made to all selected objects.
### BulkDeleteView
Deletes multiple objects. The user selects the objects to be deleted and confirms the deletion.
## Component Views
### ComponentCreateView
Create one or more component objects beloning to a parent object (e.g. interfaces attached to a device).
### ComponentEditView
A subclass of `ObjectEditView`: Updates an individual component object.
### ComponentDeleteView
A subclass of `ObjectDeleteView`: Deletes an individual component object.
### BulkComponentCreateView
Create a set of components objects for each of a selected set of parent objects. This view can be used e.g. to create multiple interfaces on multiple devices at once.

View File

@@ -72,7 +72,8 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
# You can map user attributes to Django attributes as so.
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn"
"last_name": "sn",
"email": "mail"
}
```
@@ -108,12 +109,3 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
It is also possible map user attributes to Django attributes:
```python
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
}
```

View File

@@ -102,7 +102,7 @@ Python 2:
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
```no-highlight
# pip install napalm
# pip3 install napalm
```
# Configuration

View File

@@ -1,13 +1,9 @@
# Web Server Installation
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
```no-highlight
# apt-get install -y gunicorn supervisor
```
# Web Server Installation
## Option A: nginx
@@ -55,7 +51,7 @@ Restart the nginx service to use the new configuration.
# service nginx restart
```
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04).
## Option B: Apache
@@ -100,10 +96,16 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
# service apache2 restart
```
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
# gunicorn Installation
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
```no-highlight
# pip3 install gunicorn
```
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight
@@ -116,6 +118,12 @@ user = 'www-data'
# supervisord Installation
Install supervisor:
```no-highlight
# apt-get install -y supervisor
```
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight

View File

@@ -18,6 +18,7 @@ pages:
- 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md'
- 'Virtualization': 'data-model/virtualization.md'
- 'Extras': 'data-model/extras.md'
- 'API':
- 'Overview': 'api/overview.md'
@@ -26,6 +27,8 @@ pages:
- 'Examples': 'api/examples.md'
- 'Shell':
- 'Introduction': 'shell/intro.md'
- 'Development':
- 'Utility Views': 'development/utility-views.md'
markdown_extensions:
- admonition:

View File

@@ -6,7 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
from utilities.api import ValidatedModelSerializer
#
@@ -45,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types
#
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitType
@@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
]
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -170,6 +170,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
to_field_name='slug'
)
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
#

View File

@@ -13,22 +13,6 @@ from utilities.models import CreatedUpdatedModel
from .constants import *
def humanize_speed(speed):
"""
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
"""
if speed >= 1000000000 and speed % 1000000000 == 0:
return '{} Tbps'.format(speed / 1000000000)
elif speed >= 1000000 and speed % 1000000 == 0:
return '{} Gbps'.format(speed / 1000000)
elif speed >= 1000 and speed % 1000 == 0:
return '{} Mbps'.format(speed / 1000)
elif speed >= 1000:
return '{} Mbps'.format(float(speed) / 1000)
else:
return '{} Kbps'.format(speed)
@python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel):
"""
@@ -139,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def termination_z(self):
return self._get_termination('Z')
def commit_rate_human(self):
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible
class CircuitTermination(models.Model):
@@ -173,11 +153,3 @@ class CircuitTermination(models.Model):
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
except CircuitTermination.DoesNotExist:
return None
def port_speed_human(self):
return humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'

View File

@@ -52,7 +52,7 @@ class ProviderView(View):
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_provider'
model = Provider
form_class = forms.ProviderForm
model_form = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list'
@@ -104,7 +104,7 @@ class CircuitTypeListView(ObjectListView):
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
model = CircuitType
form_class = forms.CircuitTypeForm
model_form = forms.CircuitTypeForm
def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list')
@@ -160,7 +160,7 @@ class CircuitView(View):
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuit'
model = Circuit
form_class = forms.CircuitForm
model_form = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list'
@@ -253,7 +253,7 @@ def circuit_terminations_swap(request, pk):
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittermination'
model = CircuitTermination
form_class = forms.CircuitTerminationForm
model_form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs):

View File

@@ -15,7 +15,8 @@ from dcim.models import (
)
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster
#
@@ -38,7 +39,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRegionSerializer(ValidatedModelSerializer):
class Meta:
model = Region
@@ -100,7 +101,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackGroupSerializer(ValidatedModelSerializer):
class Meta:
model = RackGroup
@@ -111,7 +112,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
# Rack roles
#
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RackRoleSerializer(ValidatedModelSerializer):
class Meta:
model = RackRole
@@ -216,7 +217,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta:
model = RackReservation
@@ -227,7 +228,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
# Manufacturers
#
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class ManufacturerSerializer(ValidatedModelSerializer):
class Meta:
model = Manufacturer
@@ -292,7 +293,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
@@ -311,7 +312,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
@@ -330,7 +331,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
@@ -349,7 +350,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
@@ -369,7 +370,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceTemplate
@@ -388,7 +389,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
@@ -399,7 +400,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
# Device roles
#
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceRole
@@ -418,7 +419,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms
#
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class PlatformSerializer(ValidatedModelSerializer):
class Meta:
model = Platform
@@ -446,6 +447,15 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'family', 'address']
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta:
model = Cluster
fields = ['id', 'url', 'name']
class DeviceSerializer(CustomFieldModelSerializer):
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
@@ -459,13 +469,14 @@ class DeviceSerializer(CustomFieldModelSerializer):
primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer()
class Meta:
model = Device
fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'comments', 'custom_fields',
'cluster', 'comments', 'custom_fields',
]
def get_parent_device(self, obj):
@@ -516,7 +527,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPort
@@ -536,7 +547,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePort
@@ -556,7 +567,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutlet
@@ -576,7 +587,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPort
@@ -664,7 +675,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
]
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta:
model = Interface
@@ -694,7 +705,7 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBay
@@ -717,7 +728,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
]
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer):
class Meta:
model = InventoryItem
@@ -749,7 +760,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceConnection

View File

@@ -3,7 +3,6 @@ from collections import OrderedDict
from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
@@ -21,7 +20,7 @@ from dcim import filters
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import ServiceUnavailable, WritableSerializerMixin
from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
from .exceptions import MissingFilterException
from . import serializers
@@ -272,15 +271,17 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
ip_address = str(device.primary_ip.address.ip)
d = driver(
hostname=ip_address,
username=settings.NETBOX_USERNAME,
password=settings.NETBOX_PASSWORD
username=settings.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
optional_args=settings.NAPALM_ARGS
)
try:
d.open()
for method in napalm_methods:
response[method] = getattr(d, method)()
except Exception as e:
raise ServiceUnavailable("Error connecting to the device: {}".format(e))
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
d.close()
return Response(response)
@@ -385,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticatedOrLoginNotRequired]
def get_view_name(self):
return "Connected Device Locator"

View File

@@ -58,6 +58,7 @@ IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_CX4 = 1170
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
@@ -92,13 +93,15 @@ IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
VIFACE_FF_CHOICES = [
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
VIFACE_FF_CHOICES,
],
[
'Ethernet (fixed)',
@@ -106,6 +109,7 @@ IFACE_FF_CHOICES = [
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
],
[

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
@@ -8,7 +9,8 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
from virtualization.models import Cluster
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
@@ -113,6 +115,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
facility_id = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -156,7 +159,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Rack
fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
fields = ['type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value):
if not value.strip():
@@ -271,6 +274,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
name='device_type_id',
label='Device type (ID)',
)
@@ -383,6 +387,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Platform (slug)',
)
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -403,6 +409,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(),
label='Rack (ID)',
)
cluster_id = NullableModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
)
model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug',
queryset=DeviceType.objects.all(),
@@ -439,7 +449,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Device
fields = ['name', 'serial', 'asset_tag']
fields = ['serial']
def search(self, queryset, name, value):
if not value.strip():
@@ -457,7 +467,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value:
return queryset
try:
return queryset.filter(interfaces__mac_address=value).distinct()
mac = EUI(value.strip())
return queryset.filter(interfaces__mac_address=mac).distinct()
except AddrFormatError:
return queryset.none()
@@ -569,7 +580,8 @@ class InterfaceFilter(django_filters.FilterSet):
if not value:
return queryset
try:
return queryset.filter(mac_address=value)
mac = EUI(value.strip())
return queryset.filter(mac_address=mac)
except AddrFormatError:
return queryset.none()
@@ -596,10 +608,11 @@ class InventoryItemFilter(DeviceComponentFilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
asset_tag = NullableCharFieldFilter()
class Meta:
model = InventoryItem
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
fields = ['name', 'part_id', 'serial', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -13,9 +13,9 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField,
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
SlugField, FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
from .models import (
@@ -28,12 +28,6 @@ from .models import (
)
FORM_STATUS_CHOICES = [
['', '---------'],
]
FORM_STATUS_CHOICES += STATUS_CHOICES
DEVICE_BY_PK_RE = '{\d+\}'
@@ -49,15 +43,6 @@ def get_device_by_name_or_pk(name):
return device
class DeviceComponentForm(BootstrapMixin, forms.Form):
"""
Allow inclusion of the parent device as context for limiting field choices.
"""
def __init__(self, device, *args, **kwargs):
self.device = device
super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
@@ -452,7 +437,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class ConsolePortTemplateCreateForm(DeviceComponentForm):
class ConsolePortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -466,7 +451,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
class ConsoleServerPortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -480,7 +465,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class PowerPortTemplateCreateForm(DeviceComponentForm):
class PowerPortTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -494,7 +479,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
}
class PowerOutletTemplateCreateForm(DeviceComponentForm):
class PowerOutletTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -508,7 +493,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
}
class InterfaceTemplateCreateForm(DeviceComponentForm):
class InterfaceTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
@@ -533,7 +518,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
}
class DeviceBayTemplateCreateForm(DeviceComponentForm):
class DeviceBayTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -642,13 +627,28 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# Compile list of choices for primary IPv4 and IPv6 addresses
for family in [4, 6]:
ip_choices = []
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
.select_related('nat_inside__interface')
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
ip_choices = [(None, '---------')]
# Collect interface IPs
interface_ips = IPAddress.objects.select_related('interface').filter(
family=family, interface__device=self.instance
)
if interface_ips:
ip_choices.append(
('Interface IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
])
)
# Collect NAT IPs
nat_ips = IPAddress.objects.select_related('nat_inside').filter(
family=family, nat_inside__interface__device=self.instance
)
if nat_ips:
ip_choices.append(
('NAT IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
])
)
self.fields['primary_ip{}'.format(family)].choices = ip_choices
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
# can be flipped from one face to another.
@@ -848,7 +848,7 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='')
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
class Meta:
@@ -912,11 +912,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
name_pattern = ExpandableNameField(label='Name')
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
class Meta:
model = Interface
fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False, initial=True)
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False)
#
@@ -933,7 +934,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
}
class ConsolePortCreateForm(DeviceComponentForm):
class ConsolePortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1102,7 +1103,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
}
class ConsoleServerPortCreateForm(DeviceComponentForm):
class ConsoleServerPortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1194,7 +1195,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
}
class PowerPortCreateForm(DeviceComponentForm):
class PowerPortCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1363,7 +1364,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
}
class PowerOutletCreateForm(DeviceComponentForm):
class PowerOutletCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
@@ -1468,7 +1469,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
)
class InterfaceCreateForm(DeviceComponentForm):
class InterfaceCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False)
@@ -1487,9 +1488,9 @@ class InterfaceCreateForm(DeviceComponentForm):
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device
if self.device is not None:
if self.parent is not None:
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
device=self.device, form_factor=IFACE_FF_LAG
device=self.parent, form_factor=IFACE_FF_LAG
)
else:
self.fields['lag'].queryset = Interface.objects.none()
@@ -1715,7 +1716,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
}
class DeviceBayCreateForm(DeviceComponentForm):
class DeviceBayCreateForm(ComponentForm):
name_pattern = ExpandableNameField(label='Name')

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0041_napalm_integration'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0042_interface_ff_10ge_cx4'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='interface',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='interfacetemplate',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='poweroutlet',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerporttemplate',
name='name',
field=models.CharField(max_length=50),
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 14:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('dcim', '0043_device_component_name_lengths'),
]
operations = [
migrations.AddField(
model_name='device',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
),
migrations.AddField(
model_name='interface',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='interface',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
),
]

View File

@@ -357,6 +357,16 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
return list(reversed(available_units))
def get_reserved_units(self):
"""
Return a dictionary mapping all reserved units within the rack to their reservation.
"""
reserved_units = {}
for r in self.reservations.all():
for u in r.units:
reserved_units[u] = r
return reserved_units
def get_0u_devices(self):
return self.devices.filter(position=0)
@@ -564,7 +574,7 @@ class ConsolePortTemplate(models.Model):
A template for a ConsolePort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -580,7 +590,7 @@ class ConsoleServerPortTemplate(models.Model):
A template for a ConsoleServerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -596,7 +606,7 @@ class PowerPortTemplate(models.Model):
A template for a PowerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -612,7 +622,7 @@ class PowerOutletTemplate(models.Model):
A template for a PowerOutlet to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -675,7 +685,7 @@ class InterfaceTemplate(models.Model):
A template for a physical data interface on a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
@@ -695,7 +705,7 @@ class DeviceBayTemplate(models.Model):
A template for a DeviceBay to be created for a new parent Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -798,6 +808,13 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='Primary IPv6'
)
cluster = models.ForeignKey(
to='virtualization.Cluster',
on_delete=models.SET_NULL,
related_name='devices',
blank=True,
null=True
)
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
@@ -880,6 +897,30 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
except DeviceType.DoesNotExist:
pass
# Validate primary IPv4 address
if self.primary_ip4 and (
self.primary_ip4.interface is None or
self.primary_ip4.interface.device != self
) and (
self.primary_ip4.nat_inside.interface is None or
self.primary_ip4.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip4),
})
# Validate primary IPv6 address
if self.primary_ip6 and (
self.primary_ip6.interface is None or
self.primary_ip6.interface.device != self
) and (
self.primary_ip6.nat_inside.interface is None or
self.primary_ip6.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})
def save(self, *args, **kwargs):
is_new = not bool(self.pk)
@@ -990,7 +1031,7 @@ class ConsolePort(models.Model):
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
"""
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1040,7 +1081,7 @@ class ConsoleServerPort(models.Model):
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
"""
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = ConsoleServerPortManager()
@@ -1061,7 +1102,7 @@ class PowerPort(models.Model):
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
"""
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1105,7 +1146,7 @@ class PowerOutlet(models.Model):
A physical power outlet (output) within a Device which provides power to a PowerPort.
"""
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = PowerOutletManager()
@@ -1123,19 +1164,32 @@ class PowerOutlet(models.Model):
@python_2_unicode_compatible
class Interface(models.Model):
"""
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
of an InterfaceConnection.
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
Interface via the creation of an InterfaceConnection.
"""
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
device = models.ForeignKey(
to='Device',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
lag = models.ForeignKey(
'self',
models.SET_NULL,
to='self',
on_delete=models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
@@ -1158,6 +1212,18 @@ class Interface(models.Model):
def clean(self):
# An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("An interface must belong to either a device or a virtual machine.")
# VM interfaces must be virtual
if self.virtual_machine and self.form_factor not in VIRTUAL_IFACE_TYPES:
raise ValidationError({
'form_factor': "Virtual machines cannot have physical interfaces."
})
# Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({
@@ -1187,6 +1253,10 @@ class Interface(models.Model):
)
})
@property
def parent(self):
return self.device or self.virtual_machine
@property
def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES

View File

@@ -126,7 +126,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports

View File

@@ -23,7 +23,8 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_S
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
@@ -60,87 +61,6 @@ def expand_pattern(string):
yield "{0}{1}".format(lead, i)
class ComponentCreateView(View):
parent_model = None
parent_field = None
model = None
form = None
model_form = None
def get(self, request, pk):
parent = get_object_or_404(self.parent_model, pk=pk)
form = self.form(parent, initial=request.GET)
return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})
def post(self, request, pk):
parent = get_object_or_404(self.parent_model, pk=pk)
form = self.form(parent, request.POST)
if form.is_valid():
new_components = []
data = deepcopy(form.cleaned_data)
for name in form.cleaned_data['name_pattern']:
component_data = {
self.parent_field: parent.pk,
'name': name,
}
# Replace objects with their primary key to keep component_form.clean() happy
for k, v in data.items():
if hasattr(v, 'pk'):
component_data[k] = v.pk
else:
component_data[k] = v
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
# Assign errors on the child form's name field to name_pattern on the parent form
if field == 'name':
field = 'name_pattern'
for e in errors:
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
if not form.errors:
self.model.objects.bulk_create(new_components)
messages.success(request, "Added {} {} to {}.".format(
len(new_components), self.model._meta.verbose_name_plural, parent
))
if '_addanother' in request.POST:
return redirect(request.path)
else:
return redirect(parent.get_absolute_url())
return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})
class ComponentEditView(ObjectEditView):
def get_return_url(self, request, obj):
return obj.device.get_absolute_url()
class ComponentDeleteView(ObjectDeleteView):
def get_return_url(self, request, obj):
return obj.device.get_absolute_url()
class BulkDisconnectView(View):
"""
An extendable view for disconnection console/power/interface components in bulk.
@@ -192,7 +112,7 @@ class RegionListView(ObjectListView):
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_region'
model = Region
form_class = forms.RegionForm
model_form = forms.RegionForm
def get_return_url(self, request, obj):
return reverse('dcim:region_list')
@@ -250,7 +170,7 @@ class SiteView(View):
class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_site'
model = Site
form_class = forms.SiteForm
model_form = forms.SiteForm
template_name = 'dcim/site_edit.html'
default_return_url = 'dcim:site_list'
@@ -297,7 +217,7 @@ class RackGroupListView(ObjectListView):
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
model_form = forms.RackGroupForm
def get_return_url(self, request, obj):
return reverse('dcim:rackgroup_list')
@@ -329,7 +249,7 @@ class RackRoleListView(ObjectListView):
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackrole'
model = RackRole
form_class = forms.RackRoleForm
model_form = forms.RackRoleForm
def get_return_url(self, request, obj):
return reverse('dcim:rackrole_list')
@@ -417,15 +337,10 @@ class RackView(View):
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
reservations = RackReservation.objects.filter(rack=rack)
reserved_units = {}
for r in reservations:
for u in r.units:
reserved_units[u] = r
return render(request, 'dcim/rack.html', {
'rack': rack,
'reservations': reservations,
'reserved_units': reserved_units,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
@@ -437,7 +352,7 @@ class RackView(View):
class RackCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rack'
model = Rack
form_class = forms.RackForm
model_form = forms.RackForm
template_name = 'dcim/rack_edit.html'
default_return_url = 'dcim:rack_list'
@@ -493,7 +408,7 @@ class RackReservationListView(ObjectListView):
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackreservation'
model = RackReservation
form_class = forms.RackReservationForm
model_form = forms.RackReservationForm
def alter_obj(self, obj, request, args, kwargs):
if not obj.pk:
@@ -537,7 +452,7 @@ class ManufacturerListView(ObjectListView):
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_manufacturer'
model = Manufacturer
form_class = forms.ManufacturerForm
model_form = forms.ManufacturerForm
def get_return_url(self, request, obj):
return reverse('dcim:manufacturer_list')
@@ -622,7 +537,7 @@ class DeviceTypeView(View):
class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicetype'
model = DeviceType
form_class = forms.DeviceTypeForm
model_form = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html'
default_return_url = 'dcim:devicetype_list'
@@ -667,6 +582,7 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = ConsolePortTemplate
form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -685,6 +601,7 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -701,6 +618,7 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerPortTemplate
form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -717,6 +635,7 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = PowerOutletTemplate
form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -733,6 +652,7 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = InterfaceTemplate
form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm
template_name = 'dcim/device_component_add.html'
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
@@ -757,6 +677,7 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/device_component_add.html'
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -779,7 +700,7 @@ class DeviceRoleListView(ObjectListView):
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicerole'
model = DeviceRole
form_class = forms.DeviceRoleForm
model_form = forms.DeviceRoleForm
def get_return_url(self, request, obj):
return reverse('dcim:devicerole_list')
@@ -810,7 +731,7 @@ class PlatformListView(ObjectListView):
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_platform'
model = Platform
form_class = forms.PlatformForm
model_form = forms.PlatformForm
def get_return_url(self, request, obj):
return reverse('dcim:platform_list')
@@ -941,7 +862,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
device = get_object_or_404(Device, pk=pk)
interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering
).filter(
).connectable().filter(
device=device
).select_related(
'connected_as_a', 'connected_as_b'
@@ -968,7 +889,7 @@ class DeviceConfigView(PermissionRequiredMixin, View):
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device
form_class = forms.DeviceForm
model_form = forms.DeviceForm
template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list'
@@ -1040,6 +961,7 @@ class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = ConsolePort
form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_consoleport')
@@ -1121,12 +1043,14 @@ def consoleport_disconnect(request, pk):
class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleport'
model = ConsolePort
form_class = forms.ConsolePortForm
parent_field = 'device'
model_form = forms.ConsolePortForm
class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleport'
model = ConsolePort
parent_field = 'device'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -1154,6 +1078,7 @@ class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = ConsoleServerPort
form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_consoleserverport')
@@ -1238,12 +1163,14 @@ def consoleserverport_disconnect(request, pk):
class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
form_class = forms.ConsoleServerPortForm
parent_field = 'device'
model_form = forms.ConsoleServerPortForm
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleserverport'
model = ConsoleServerPort
parent_field = 'device'
class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1273,6 +1200,7 @@ class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerPort
form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_powerport')
@@ -1354,12 +1282,14 @@ def powerport_disconnect(request, pk):
class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_powerport'
model = PowerPort
form_class = forms.PowerPortForm
parent_field = 'device'
model_form = forms.PowerPortForm
class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_powerport'
model = PowerPort
parent_field = 'device'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -1387,6 +1317,7 @@ class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerOutlet
form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm
template_name = 'dcim/device_component_add.html'
@permission_required('dcim.change_poweroutlet')
@@ -1471,12 +1402,14 @@ def poweroutlet_disconnect(request, pk):
class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
form_class = forms.PowerOutletForm
parent_field = 'device'
model_form = forms.PowerOutletForm
class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_poweroutlet'
model = PowerOutlet
parent_field = 'device'
class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1508,17 +1441,20 @@ class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
model = Interface
form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm
template_name = 'dcim/device_component_add.html'
class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_interface'
model = Interface
form_class = forms.InterfaceForm
parent_field = 'device'
model_form = forms.InterfaceForm
class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_interface'
model = Interface
parent_field = 'device'
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
@@ -1559,17 +1495,20 @@ class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
model = DeviceBay
form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm
template_name = 'dcim/device_component_add.html'
class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_devicebay'
model = DeviceBay
form_class = forms.DeviceBayForm
parent_field = 'device'
model_form = forms.DeviceBayForm
class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_devicebay'
model = DeviceBay
parent_field = 'device'
@permission_required('dcim.change_devicebay')
@@ -1630,109 +1569,67 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
# Bulk device component creation
# Bulk Device component creation
#
class DeviceBulkAddComponentView(View):
"""
Add one or more components (e.g. interfaces) to a selected set of Devices.
"""
form = forms.DeviceBulkAddComponentForm
model = None
model_form = None
def get(self):
return redirect('dcim:device_list')
def post(self, request):
# Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'):
pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
if '_create' in request.POST:
form = self.form(request.POST)
if form.is_valid():
new_components = []
data = deepcopy(form.cleaned_data)
for device in data['pk']:
names = data['name_pattern']
for name in names:
component_data = {
'device': device.pk,
'name': name,
}
component_data.update(data)
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
for e in errors:
form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e)))
if not form.errors:
self.model.objects.bulk_create(new_components)
messages.success(request, "Added {} {} to {} devices.".format(
len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
))
return redirect('dcim:device_list')
else:
form = self.form(initial={'pk': pk_list})
selected_devices = Device.objects.filter(pk__in=pk_list)
if not selected_devices:
messages.warning(request, "No devices were selected.")
return redirect('dcim:device_list')
return render(request, 'dcim/device_bulk_add_component.html', {
'form': form,
'component_name': self.model._meta.verbose_name_plural,
'selected_devices': selected_devices,
'return_url': reverse('dcim:device_list'),
})
class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = ConsolePort
model_form = forms.ConsolePortForm
table = tables.DeviceTable
class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = ConsoleServerPort
model_form = forms.ConsoleServerPortForm
table = tables.DeviceTable
class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = PowerPort
model_form = forms.PowerPortForm
table = tables.DeviceTable
class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = PowerOutlet
model_form = forms.PowerOutletForm
table = tables.DeviceTable
class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddInterfaceForm
model = Interface
model_form = forms.InterfaceForm
table = tables.DeviceTable
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'
form = forms.DeviceBulkAddComponentForm
model = DeviceBay
model_form = forms.DeviceBayForm
table = tables.DeviceTable
#
@@ -1876,7 +1773,8 @@ class InterfaceConnectionsListView(ObjectListView):
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_inventoryitem'
model = InventoryItem
form_class = forms.InventoryItemForm
parent_field = 'device'
model_form = forms.InventoryItemForm
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs:
@@ -1887,3 +1785,4 @@ class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_inventoryitem'
model = InventoryItem
parent_field = 'device'

View File

@@ -10,6 +10,7 @@ from django.db import transaction
from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
)
from utilities.api import ValidatedModelSerializer
#
@@ -68,7 +69,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
return data
class CustomFieldModelSerializer(serializers.ModelSerializer):
class CustomFieldModelSerializer(ValidatedModelSerializer):
"""
Extends ModelSerializer to render any CustomFields and their values associated with an object.
"""
@@ -111,16 +112,6 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)},
)
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -10,7 +10,7 @@ from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
)
from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
#
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:

View File

@@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand):
help = "Update inventory information for specified devices"
username = settings.NETBOX_USERNAME
password = settings.NETBOX_PASSWORD
username = settings.NAPALM_USERNAME
password = settings.NAPALM_PASSWORD
def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

View File

@@ -285,7 +285,7 @@ class TopologyMap(models.Model):
# Add each device to the graph
devices = []
for query in device_set.split(';'): # Split regexes on semicolons
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query).select_related('device_role')
for d in devices:
bg_color = '#{}'.format(d.device_role.color)

View File

@@ -11,7 +11,7 @@ from .models import ImageAttachment
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_imageattachment'
model = ImageAttachment
form_class = ImageAttachmentForm
model_form = ImageAttachmentForm
def alter_obj(self, imageattachment, request, args, kwargs):
if not imageattachment.pk:

View File

@@ -11,7 +11,8 @@ from ipam.models import (
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.api.serializers import NestedVirtualMachineSerializer
#
@@ -45,7 +46,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles
#
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RoleSerializer(ValidatedModelSerializer):
class Meta:
model = Role
@@ -64,7 +65,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs
#
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RIRSerializer(ValidatedModelSerializer):
class Meta:
model = RIR
@@ -295,17 +296,18 @@ class AvailableIPSerializer(serializers.Serializer):
class ServiceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
virtual_machine = NestedVirtualMachineSerializer()
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
ipaddresses = NestedIPAddressSerializer(many=True)
class Meta:
model = Service
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']

View File

@@ -115,7 +115,11 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = list(prefix.get_available_ips())[:limit]
ip_list = []
for index, ip in enumerate(prefix.get_available_ips(), start=1):
ip_list.append(ip)
if index == limit:
break
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,
@@ -130,7 +134,11 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
#
class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
queryset = IPAddress.objects.select_related(
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
'interface__device'
)
serializer_class = serializers.IPAddressSerializer
write_serializer_class = serializers.WritableIPAddressSerializer
filter_class = filters.IPAddressFilter

View File

@@ -70,7 +70,7 @@
"family": 4,
"address": "10.0.255.1/32",
"vrf": null,
"interface": 3,
"interface_id": 3,
"nat_inside": null,
"description": ""
}
@@ -84,7 +84,7 @@
"family": 4,
"address": "169.254.254.1/31",
"vrf": null,
"interface": 4,
"interface_id": 4,
"nat_inside": null,
"description": ""
}
@@ -98,7 +98,7 @@
"family": 4,
"address": "10.0.255.2/32",
"vrf": null,
"interface": 185,
"interface_id": 185,
"nat_inside": null,
"description": ""
}
@@ -112,7 +112,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"interface": 213,
"interface_id": 213,
"nat_inside": null,
"description": ""
}
@@ -126,7 +126,7 @@
"family": 4,
"address": "10.0.254.1/24",
"vrf": null,
"interface": 12,
"interface_id": 12,
"nat_inside": null,
"description": ""
}
@@ -140,7 +140,7 @@
"family": 4,
"address": "10.15.21.1/31",
"vrf": null,
"interface": 218,
"interface_id": 218,
"nat_inside": null,
"description": ""
}
@@ -154,7 +154,7 @@
"family": 4,
"address": "10.15.21.2/31",
"vrf": null,
"interface": 9,
"interface_id": 9,
"nat_inside": null,
"description": ""
}
@@ -168,7 +168,7 @@
"family": 4,
"address": "10.15.22.1/31",
"vrf": null,
"interface": 8,
"interface_id": 8,
"nat_inside": null,
"description": ""
}
@@ -182,7 +182,7 @@
"family": 4,
"address": "10.15.20.1/31",
"vrf": null,
"interface": 7,
"interface_id": 7,
"nat_inside": null,
"description": ""
}
@@ -196,7 +196,7 @@
"family": 4,
"address": "10.16.20.1/31",
"vrf": null,
"interface": 216,
"interface_id": 216,
"nat_inside": null,
"description": ""
}
@@ -210,7 +210,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"interface": 206,
"interface_id": 206,
"nat_inside": null,
"description": ""
}
@@ -224,7 +224,7 @@
"family": 4,
"address": "10.16.22.1/31",
"vrf": null,
"interface": 217,
"interface_id": 217,
"nat_inside": null,
"description": ""
}
@@ -238,7 +238,7 @@
"family": 4,
"address": "10.16.22.2/31",
"vrf": null,
"interface": 205,
"interface_id": 205,
"nat_inside": null,
"description": ""
}
@@ -252,7 +252,7 @@
"family": 4,
"address": "10.16.20.2/31",
"vrf": null,
"interface": 211,
"interface_id": 211,
"nat_inside": null,
"description": ""
}
@@ -266,7 +266,7 @@
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"interface": 212,
"interface_id": 212,
"nat_inside": null,
"description": ""
}
@@ -280,7 +280,7 @@
"family": 4,
"address": "10.0.254.2/32",
"vrf": null,
"interface": 188,
"interface_id": 188,
"nat_inside": null,
"description": ""
}
@@ -294,7 +294,7 @@
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"interface": 200,
"interface_id": 200,
"nat_inside": null,
"description": ""
}
@@ -308,7 +308,7 @@
"family": 4,
"address": "169.254.1.2/31",
"vrf": null,
"interface": 194,
"interface_id": 194,
"nat_inside": null,
"description": ""
}

View File

@@ -25,11 +25,8 @@ IP_FAMILY_CHOICES = [
(6, 'IPv6'),
]
PREFIX_MASK_LENGTH_CHOICES = [
('', '---------'),
] + [(i, i) for i in range(1, 128)]
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
#
@@ -377,50 +374,9 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
interface_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'interface_rack'}
)
)
interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'interface_site'),
),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
)
)
interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'interface_site'),
('rack', 'interface_rack'),
),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
chains=(
('device', 'interface_device'),
),
required=False,
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
)
required=False
)
nat_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
@@ -479,13 +435,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
obj_label='address'
)
)
primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant',
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
'nat_rack', 'nat_inside', 'tenant_group', 'tenant',
]
def __init__(self, *args, **kwargs):
@@ -493,10 +449,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside and instance.nat_inside.device is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
@@ -507,22 +459,30 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
self.fields['vrf'].empty_label = 'Global'
# Initialize primary_for_device if IP address is already assigned
if self.instance.interface is not None:
device = self.instance.interface.device
# Limit interface selections to those belonging to the parent device/VM
if self.instance and self.instance.interface:
self.fields['interface'].queryset = Interface.objects.filter(
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
)
else:
self.fields['interface'].choices = []
# Initialize primary_for_parent if IP address is already assigned
if self.instance.pk and self.instance.interface is not None:
parent = self.instance.interface.parent
if (
self.instance.address.version == 4 and device.primary_ip4 == self.instance or
self.instance.address.version == 6 and device.primary_ip6 == self.instance
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
):
self.initial['primary_for_device'] = True
self.initial['primary_for_parent'] = True
def clean(self):
super(IPAddressForm, self).clean()
# Primary IP assignment is only available if an interface has been assigned.
if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
self.add_error(
'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
)
def save(self, *args, **kwargs):
@@ -530,13 +490,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
# Assign this IPAddress as the primary for the associated Device.
if self.cleaned_data['primary_for_device']:
device = self.cleaned_data['interface'].device
if self.cleaned_data['primary_for_parent']:
parent = self.cleaned_data['interface'].parent
if ipaddress.address.version == 4:
device.primary_ip4 = ipaddress
parent.primary_ip4 = ipaddress
else:
device.primary_ip6 = ipaddress
device.save()
parent.primary_ip6 = ipaddress
parent.save()
# Clear assignment as primary for device if set.
else:
@@ -554,7 +514,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
return ipaddress
class IPAddressPatternForm(BootstrapMixin, forms.Form):
class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
pattern = ExpandableIPAddressField(label='Address pattern')
@@ -898,5 +858,14 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
super(ServiceForm, self).__init__(*args, **kwargs)
# Limit IP address choices to those assigned to interfaces of the parent device
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)
# Limit IP address choices to those assigned to interfaces of the parent device/VM
if self.instance.device:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
interface__device=self.instance.device
)
elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
interface__virtual_machine=self.instance.virtual_machine
)
else:
self.fields['ipaddresses'].choices = []

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-03 19:37
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0017_ipaddress_roles'),
]
operations = [
migrations.AlterUniqueTogether(
name='service',
unique_together=set([]),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 15:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('ipam', '0018_remove_service_uniqueness_constraint'),
]
operations = [
migrations.AlterModelOptions(
name='service',
options={'ordering': ['protocol', 'port']},
),
migrations.AddField(
model_name='service',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='service',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
),
]

View File

@@ -2,10 +2,12 @@ from __future__ import unicode_literals
import netaddr
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Q
from django.db.models.expressions import RawSQL
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
@@ -586,21 +588,59 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
@python_2_unicode_compatible
class Service(CreatedUpdatedModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
to one or more specific IPAddresses belonging to the Device.
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent.
"""
device = models.ForeignKey('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
name = models.CharField(max_length=30)
protocol = models.PositiveSmallIntegerField(choices=IP_PROTOCOL_CHOICES)
port = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)],
verbose_name='Port number')
ipaddresses = models.ManyToManyField('ipam.IPAddress', related_name='services', blank=True,
verbose_name='IP addresses')
description = models.CharField(max_length=100, blank=True)
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='services',
verbose_name='device',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='services',
null=True,
blank=True
)
name = models.CharField(
max_length=30
)
protocol = models.PositiveSmallIntegerField(
choices=IP_PROTOCOL_CHOICES
)
port = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(65535)],
verbose_name='Port number'
)
ipaddresses = models.ManyToManyField(
to='ipam.IPAddress',
related_name='services',
blank=True,
verbose_name='IP addresses'
)
description = models.CharField(
max_length=100,
blank=True
)
class Meta:
ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
ordering = ['protocol', 'port']
def __str__(self):
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
@property
def parent(self):
return self.device or self.virtual_machine
def clean(self):
# A Service must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("A service cannot be associated with both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("A service must be associated with either a device or a virtual machine.")

View File

@@ -45,9 +45,9 @@ ROLE_ACTIONS = """
PREFIX_LINK = """
{% if record.has_children %}
<span style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
{% else %}
<span style="padding-left: {{ record.depth }}9px">
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
{% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span>
@@ -77,10 +77,9 @@ IPADDRESS_LINK = """
{% endif %}
"""
IPADDRESS_DEVICE = """
IPADDRESS_PARENT = """
{% if record.interface %}
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
({{ record.interface.name }})
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
{% else %}
&mdash;
{% endif %}
@@ -266,12 +265,12 @@ class IPAddressTable(BaseTable):
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK)
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
@@ -284,7 +283,7 @@ class IPAddressDetailTable(IPAddressTable):
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'description',
)

View File

@@ -10,12 +10,14 @@ from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import View
from dcim.models import Device
from dcim.models import Device, Interface
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST
from .models import (
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
@@ -117,7 +119,7 @@ class VRFView(View):
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vrf'
model = VRF
form_class = forms.VRFForm
model_form = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list'
@@ -249,7 +251,7 @@ class RIRListView(ObjectListView):
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir'
model = RIR
form_class = forms.RIRForm
model_form = forms.RIRForm
def get_return_url(self, request, obj):
return reverse('ipam:rir_list')
@@ -340,7 +342,7 @@ class AggregateView(View):
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_aggregate'
model = Aggregate
form_class = forms.AggregateForm
model_form = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list'
@@ -394,7 +396,7 @@ class RoleListView(ObjectListView):
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_role'
model = Role
form_class = forms.RoleForm
model_form = forms.RoleForm
def get_return_url(self, request, obj):
return reverse('ipam:role_list')
@@ -547,7 +549,7 @@ class PrefixIPAddressesView(View):
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_prefix'
model = Prefix
form_class = forms.PrefixForm
model_form = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list'
@@ -594,7 +596,11 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
queryset = IPAddress.objects.select_related(
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
'interface__device', 'interface__virtual_machine'
)
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressDetailTable
@@ -605,7 +611,7 @@ class IPAddressView(View):
def get(self, request, pk):
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk)
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(
@@ -622,12 +628,17 @@ class IPAddressView(View):
).exclude(
pk=ipaddress.pk
).select_related(
'interface__device', 'nat_inside'
'nat_inside'
).prefetch_related(
'interface__device'
)
# Exclude anycast IPs if this IP is anycast
if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
# Related IP table
related_ips = IPAddress.objects.select_related(
related_ips = IPAddress.objects.prefetch_related(
'interface__device'
).exclude(
address=str(ipaddress.address)
@@ -647,10 +658,21 @@ class IPAddressView(View):
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_ipaddress'
model = IPAddress
form_class = forms.IPAddressForm
model_form = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list'
def alter_obj(self, obj, request, url_args, url_kwargs):
interface_id = request.GET.get('interface')
if interface_id:
try:
obj.interface = Interface.objects.get(pk=interface_id)
except (ValueError, Interface.DoesNotExist):
pass
return obj
class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress'
@@ -664,7 +686,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
permission_required = 'ipam.add_ipaddress'
pattern_form = forms.IPAddressPatternForm
form = forms.IPAddressBulkCreateForm
model_form = forms.IPAddressBulkAddForm
pattern_target = 'address'
template_name = 'ipam/ipaddress_bulk_add.html'
@@ -681,7 +703,7 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress'
cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
filter = filters.IPAddressFilter
table = tables.IPAddressTable
form = forms.IPAddressBulkEditForm
@@ -691,7 +713,7 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
filter = filters.IPAddressFilter
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
@@ -712,7 +734,7 @@ class VLANGroupListView(ObjectListView):
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
model_form = forms.VLANGroupForm
def get_return_url(self, request, obj):
return reverse('ipam:vlangroup_list')
@@ -763,7 +785,7 @@ class VLANView(View):
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlan'
model = VLAN
form_class = forms.VLANForm
model_form = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list'
@@ -811,16 +833,18 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service'
model = Service
form_class = forms.ServiceForm
model_form = forms.ServiceForm
template_name = 'ipam/service_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs:
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
elif 'virtualmachine' in url_kwargs:
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
return obj
def get_return_url(self, request, obj):
return obj.device.get_absolute_url()
return obj.parent.get_absolute_url()
class ServiceEditView(ServiceCreateView):

View File

@@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '')
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
# Credentials that NetBox will use to access live devices.
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '')
NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '')
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)

View File

@@ -93,9 +93,20 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000
# Credentials that NetBox will use to access live devices (future use).
NETBOX_USERNAME = ''
NETBOX_PASSWORD = ''
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
# the default value of this setting is derived from the installed location.
# MEDIA_ROOT = '/opt/netbox/netbox/media'
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = ''
NAPALM_PASSWORD = ''
# NAPALM timeout (in seconds). (Default: 30)
NAPALM_TIMEOUT = 30
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
# be provided as a dictionary.
NAPALM_ARGS = {}
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = 50

View File

@@ -35,7 +35,7 @@ OBJ_TYPE_CHOICES = (
class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField(
label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'})
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'})
)
obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

@@ -13,7 +13,9 @@ except ImportError:
)
VERSION = '2.1.0'
VERSION = '2.2-beta1'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -44,10 +46,15 @@ LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -56,6 +63,19 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Check for deprecated configuration parameters
config_logger = logging.getLogger('configuration')
config_logger.addHandler(logging.StreamHandler())
config_logger.setLevel(logging.WARNING)
if NETBOX_USERNAME:
config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.')
if not NAPALM_USERNAME:
NAPALM_USERNAME = NETBOX_USERNAME
if NETBOX_PASSWORD:
config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.')
if not NAPALM_PASSWORD:
NAPALM_PASSWORD = NETBOX_PASSWORD
# Attempt to import LDAP configuration if it has been defined
LDAP_IGNORE_CERT_ERRORS = False
try:
@@ -78,17 +98,15 @@ if LDAP_CONFIGURED:
if LDAP_IGNORE_CERT_ERRORS:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Enable logging for django_auth_ldap
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
ldap_logger = logging.getLogger('django_auth_ldap')
ldap_logger.addHandler(logging.StreamHandler())
ldap_logger.setLevel(logging.DEBUG)
except ImportError:
raise ImproperlyConfigured(
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
"netbox/ldap_config.py to disable LDAP."
)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Database
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})
DATABASES = {
@@ -128,6 +146,7 @@ INSTALLED_APPS = (
'tenancy',
'users',
'utilities',
'virtualization',
)
# Middleware
@@ -184,7 +203,6 @@ STATICFILES_DIRS = (
)
# Media
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/{}media/'.format(BASE_PATH)
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
@@ -204,6 +222,7 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
# Django REST framework (API)
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
REST_FRAMEWORK = {
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'utilities.api.TokenAuthentication',
@@ -216,9 +235,9 @@ REST_FRAMEWORK = {
'utilities.api.TokenPermissions',
),
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'PAGE_SIZE': PAGINATE_COUNT,
'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
}
# Django debug toolbar

View File

@@ -32,6 +32,7 @@ _patterns = [
url(r'^secrets/', include('secrets.urls')),
url(r'^tenancy/', include('tenancy.urls')),
url(r'^user/', include('users.urls')),
url(r'^virtualization/', include('virtualization.urls')),
# API
url(r'^api/$', APIRootView.as_view(), name='api-root'),
@@ -41,6 +42,7 @@ _patterns = [
url(r'^api/ipam/', include('ipam.api.urls')),
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/', swagger_view, name='api_docs'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware

View File

@@ -25,6 +25,9 @@ from secrets.tables import SecretTable
from tenancy.filters import TenantFilter
from tenancy.models import Tenant
from tenancy.tables import TenantTable
from virtualization.filters import ClusterFilter, VirtualMachineFilter
from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable
from .forms import SearchForm
@@ -90,7 +93,7 @@ SEARCH_TYPES = OrderedDict((
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant'),
'filter': IPAddressFilter,
'table': IPAddressTable,
'url': 'ipam:ipaddress_list',
@@ -115,6 +118,19 @@ SEARCH_TYPES = OrderedDict((
'table': TenantTable,
'url': 'tenancy:tenant_list',
}),
# Virtualization
('cluster', {
'queryset': Cluster.objects.all(),
'filter': ClusterFilter,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.select_related('cluster', 'tenant', 'platform'),
'filter': VirtualMachineFilter,
'table': VirtualMachineTable,
'url': 'virtualization:virtualmachine_list',
}),
))
@@ -150,6 +166,10 @@ class HomeView(View):
# Secrets
'secret_count': Secret.objects.count(),
# Virtualization
'cluster_count': Cluster.objects.count(),
'virtualmachine_count': VirtualMachine.objects.count(),
}
return render(request, self.template_name, {
@@ -216,14 +236,15 @@ class APIRootView(APIView):
def get(self, request, format=None):
return Response({
'circuits': reverse('circuits-api:api-root', request=request, format=format),
'dcim': reverse('dcim-api:api-root', request=request, format=format),
'extras': reverse('extras-api:api-root', request=request, format=format),
'ipam': reverse('ipam-api:api-root', request=request, format=format),
'secrets': reverse('secrets-api:api-root', request=request, format=format),
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
})
return Response(OrderedDict((
('circuits', reverse('circuits-api:api-root', request=request, format=format)),
('dcim', reverse('dcim-api:api-root', request=request, format=format)),
('extras', reverse('extras-api:api-root', request=request, format=format)),
('ipam', reverse('ipam-api:api-root', request=request, format=format)),
('secrets', reverse('secrets-api:api-root', request=request, format=format)),
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
)))
def handle_500(request):

View File

@@ -81,6 +81,11 @@ footer p {
}
}
/* Navigation menu */
li.subnav > a {
padding-left: 30px;
}
/* Forms */
label {
font-weight: normal;

View File

@@ -77,7 +77,9 @@ $(document).ready(function() {
// Wipe out any existing options within the child field and create a default option
child_field.empty();
child_field.append($("<option></option>").attr("value", "").text("---------"));
if (!child_field.attr('multiple')) {
child_field.append($("<option></option>").attr("value", "").text("---------"));
}
if ($(this).val() || $(this).attr('nullable') == 'true') {
var api_url = child_field.attr('api-url') + '&limit=1000';

View File

@@ -43,7 +43,7 @@ $(document).ready(function() {
success: function (response, status) {
if (response.plaintext) {
console.log("Secret retrieved successfully");
$('#secret_' + secret_id).html(response.plaintext);
$('#secret_' + secret_id).text(response.plaintext);
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
$('button.lock-secret[secret-id=' + secret_id + ']').show();
} else {

View File

@@ -5,14 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer
from secrets.models import Secret, SecretRole
from utilities.api import ModelValidationMixin
from utilities.api import ValidatedModelSerializer
#
# SecretRoles
#
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class SecretRoleSerializer(ValidatedModelSerializer):
class Meta:
model = SecretRole

View File

@@ -42,7 +42,7 @@ class SecretRoleListView(ObjectListView):
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.add_secretrole'
model = SecretRole
form_class = forms.SecretRoleForm
model_form = forms.SecretRoleForm
def get_return_url(self, request, obj):
return reverse('secrets:secretrole_list')

View File

@@ -31,197 +31,223 @@
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
<li><a href="{% url 'dcim:site_list' %}"><strong>Sites</strong></a></li>
{% if perms.dcim.add_site %}
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
<li class="subnav"><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus"></i> Add a Site</a></li>
<li class="subnav"><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download"></i> Import Sites</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
<li><a href="{% url 'dcim:region_list' %}"><strong>Regions</strong></a></li>
{% if perms.dcim.add_region %}
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
<li class="subnav"><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus"></i> Add a Region</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><strong>Tenants</strong></a></li>
{% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
<li class="subnav"><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus"></i> Add a Tenant</a></li>
<li class="subnav"><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download"></i> Import Tenants</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><strong>Tenant Groups</strong></a></li>
{% if perms.tenancy.add_tenantgroup %}
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
<li class="subnav"><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus"></i> Add a Tenant Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
<li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
<li><a href="{% url 'dcim:rack_list' %}"><strong>Racks</strong></a></li>
{% if perms.dcim.add_rack %}
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
<li class="subnav"><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus"></i> Add a Rack</a></li>
<li class="subnav"><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download"></i> Import Racks</a></li>
{% endif %}
<li><a href="{% url 'dcim:rack_elevation_list' %}"><strong>Rack Elevations</strong></a></li>
<li class="divider"></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><strong>Rack Groups</strong></a></li>
{% if perms.dcim.add_rackgroup %}
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
<li class="subnav"><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus"></i> Add a Rack Group</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><strong>Rack Roles</strong></a></li>
{% if perms.dcim.add_rackrole %}
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
<li class="subnav"><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus"></i> Add a Rack Role</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
<li><a href="{% url 'dcim:rackreservation_list' %}"><strong>Rack Reservations</strong></a></li>
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
<li><a href="{% url 'dcim:device_list' %}"><strong>Devices</strong></a></li>
{% if perms.dcim.add_device %}
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
<li class="subnav"><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus"></i> Add a Device</a></li>
<li class="subnav"><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download"></i> Import Devices</a></li>
{% endif %}
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
<li><a href="{% url 'dcim:devicetype_list' %}"><strong>Device Types</strong></a></li>
{% if perms.dcim.add_devicetype %}
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
<li class="subnav"><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus"></i> Add a Device Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><strong>Device Roles</strong></a></li>
{% if perms.dcim.add_devicerole %}
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
<li class="subnav"><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus"></i> Add a Device Role</a></li>
{% endif %}
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
<li><a href="{% url 'dcim:manufacturer_list' %}"><strong>Manufacturers</strong></a></li>
{% if perms.dcim.add_manufacturer %}
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
<li class="subnav"><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus"></i> Add a Manufacturer</a></li>
{% endif %}
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
<li><a href="{% url 'dcim:platform_list' %}"><strong>Platforms</strong></a></li>
{% if perms.dcim.add_platform %}
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
<li class="subnav"><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus"></i> Add a Platform</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_list' %}"><strong>Console Connections</strong></a></li>
{% if perms.dcim.change_consoleport %}
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
<li class="subnav"><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download"></i> Import Console Connections</a></li>
{% endif %}
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_list' %}"><strong>Power Connections</strong></a></li>
{% if perms.dcim.change_powerport %}
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
<li class="subnav"><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download"></i> Import Power Connections</a></li>
{% endif %}
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_list' %}"><strong>Interface Connections</strong></a></li>
{% if perms.dcim.add_interfaceconnection %}
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
<li class="subnav"><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download"></i> Import Interface Connections</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
<li><a href="{% url 'ipam:ipaddress_list' %}"><strong>IP Addresses</strong></a></li>
{% if perms.ipam.add_ipaddress %}
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
<li class="subnav"><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus"></i> Add an IP</a></li>
<li class="subnav"><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download"></i> Import IPs</a></li>
{% endif %}
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_list' %}"><strong>Prefixes</strong></a></li>
{% if perms.ipam.add_prefix %}
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
<li class="subnav"><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus"></i> Add a Prefix</a></li>
<li class="subnav"><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download"></i> Import Prefixes</a></li>
{% endif %}
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_list' %}"><strong>Aggregates</strong></a></li>
{% if perms.ipam.add_aggregate %}
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
<li class="subnav"><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus"></i> Add an Aggregate</a></li>
<li class="subnav"><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download"></i> Import Aggregates</a></li>
{% endif %}
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
<li><a href="{% url 'ipam:vrf_list' %}"><strong>VRFs</strong></a></li>
{% if perms.ipam.add_vrf %}
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
<li class="subnav"><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus"></i> Add a VRF</a></li>
<li class="subnav"><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download"></i> Import VRFs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
<li><a href="{% url 'ipam:rir_list' %}"><strong>RIRs</strong></a></li>
{% if perms.ipam.add_rir %}
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
<li class="subnav"><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus"></i> Add a RIR</a></li>
{% endif %}
{% if perms.ipam.add_rir or perms.ipam.add_role %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li>
<li><a href="{% url 'ipam:role_list' %}"><strong>Prefix Roles</strong></a></li>
{% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
<li><a href="{% url 'ipam:vlan_list' %}"><strong>VLANs</strong></a></li>
{% if perms.ipam.add_vlan %}
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
<li class="subnav"><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus"></i> Add a VLAN</a></li>
<li class="subnav"><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download"></i> Import VLANs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><strong>VLAN Groups</strong></a></li>
{% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
<li class="subnav"><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus"></i> Add a VLAN Group</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li>
<li><a href="{% url 'ipam:role_list' %}"><strong>VLAN Roles</strong></a></li>
{% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Virtualization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'virtualization:cluster_list' %}"><strong>Clusters</strong></a></li>
{% if perms.virtualization.add_cluster %}
<li class="subnav"><a href="{% url 'virtualization:cluster_add' %}"><i class="fa fa-plus"></i> Add a Cluster</a></li>
<li class="subnav"><a href="{% url 'virtualization:cluster_import' %}"><i class="fa fa-download"></i> Import Clusters</a></li>
{% endif %}
{% if perms.virtualization.add_cluster or perms.virtualization.add_virtualmachine %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'virtualization:virtualmachine_list' %}"><strong>Virtual Machines</strong></a></li>
{% if perms.virtualization.add_virtualmachine %}
<li class="subnav"><a href="{% url 'virtualization:virtualmachine_add' %}"><i class="fa fa-plus"></i> Add a Virtual Machine</a></li>
<li class="subnav"><a href="{% url 'virtualization:virtualmachine_import' %}"><i class="fa fa-download"></i> Import Virtual Machines</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'virtualization:clustertype_list' %}"><strong>Cluster Types</strong></a></li>
{% if perms.virtualization.add_clustertype %}
<li class="subnav"><a href="{% url 'virtualization:clustertype_add' %}"><i class="fa fa-plus"></i> Add a Cluster Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'virtualization:clustergroup_list' %}"><strong>Cluster Groups</strong></a></li>
{% if perms.virtualization.add_clustergroup %}
<li class="subnav"><a href="{% url 'virtualization:clustergroup_add' %}"><i class="fa fa-plus"></i> Add a Cluster Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
{% if perms.circuits.add_provider %}
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
{% endif %}
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
<li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
{% if perms.circuits.add_circuit %}
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
<li class="subnav"><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus"></i> Add a Circuit</a></li>
<li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
<li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
{% if perms.circuits.add_provider %}
<li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
<li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
{% if perms.circuits.add_circuittype %}
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
<li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>
{% endif %}
</ul>
</li>
@@ -229,14 +255,14 @@
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
<li><a href="{% url 'secrets:secret_list' %}"><strong>Secrets</strong></a></li>
{% if perms.secrets.add_secret %}
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
<li class="subnav"><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download"></i> Import Secrets</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><strong>Secret Roles</strong></a></li>
{% if perms.secrets.add_secretrole %}
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
<li class="subnav"><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus"></i> Add a Secret Role</a></li>
{% endif %}
</ul>
</li>
@@ -250,16 +276,16 @@
{{ request.user|truncatechars:"30" }} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> Admin</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out"></i> Log out</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
{% endif %}
</ul>
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
@@ -267,7 +293,7 @@
<input type="text" name="q" class="form-control" placeholder="Search">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search" aria-hidden="true"></i>
<i class="fa fa-search"></i>
</button>
</span>
</div>
@@ -290,7 +316,7 @@
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span>&times;</span>
</button>
{{ message }}
</div>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -39,7 +37,7 @@
</a>
{% endif %}
</div>
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=circuit %}
<div class="row">
<div class="col-md-6">
@@ -90,7 +88,7 @@
<td>Commit Rate</td>
<td>
{% if circuit.commit_rate %}
{{ circuit.commit_rate_human }}
{{ circuit.commit_rate|humanize_speed }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}

View File

@@ -9,7 +9,16 @@
{% render_field form.cid %}
{% render_field form.type %}
{% render_field form.install_date %}
{% render_field form.commit_rate %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_commit_rate">{{ form.commit_rate.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.commit_rate }}
{% include 'circuits/inc/speed_widget.html' with target_field='commit_rate' %}
</div>
<span class="help-block">{{ form.commit_rate.help_text }}</span>
</div>
</div>
{% render_field form.description %}
</div>
</div>
@@ -35,3 +44,12 @@
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$("a.set_speed").click(function(e) {
e.preventDefault();
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
});
</script>
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Circuits{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuit %}
@@ -17,7 +15,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %}
</div>
<h1>Circuits</h1>
<h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}

View File

@@ -2,10 +2,6 @@
{% load staticfiles %}
{% load form_helpers %}
{% block title %}
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -14,7 +10,7 @@
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
<h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
@@ -53,8 +49,26 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination Details</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
<div class="form-group">
<label class="col-md-3 control-label required" for="id_port_speed">{{ form.port_speed.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.port_speed }}
{% include 'circuits/inc/speed_widget.html' with target_field='port_speed' %}
</div>
<span class="help-block">{{ form.port_speed.help_text }}</span>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="id_upstream_speed">{{ form.upstream_speed.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.upstream_speed }}
{% include 'circuits/inc/speed_widget.html' with target_field='upstream_speed' %}
</div>
<span class="help-block">{{ form.upstream_speed.help_text }}</span>
</div>
</div>
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
@@ -76,4 +90,10 @@
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
<script type="text/javascript">
$("a.set_speed").click(function(e) {
e.preventDefault();
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
});
</script>
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Circuit Types{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuittype %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Circuit Types</h1>
<h1>{% block title %}Circuit Types{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}

View File

@@ -1,3 +1,5 @@
{% load helpers %}
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
@@ -49,10 +51,10 @@
<td>Speed</td>
<td>
{% if termination.upstream_speed %}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }}
{% else %}
{{ termination.port_speed_human }}
{{ termination.port_speed|humanize_speed }}
{% endif %}
</td>
</tr>

View File

@@ -0,0 +1,17 @@
<span class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="#" target="{{ target_field }}" data="10000" class="set_speed">10 Mbps</a></li>
<li><a href="#" target="{{ target_field }}" data="100000" class="set_speed">100 Mbps</a></li>
<li><a href="#" target="{{ target_field }}" data="1000000" class="set_speed">1 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="10000000" class="set_speed">10 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="25000000" class="set_speed">25 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="40000000" class="set_speed">40 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="100000000" class="set_speed">100 Gbps</a></li>
<li class="divider"></li>
<li><a href="#" target="{{ target_field }}" data="1544" class="set_speed">T1 (1.544 Mbps)</a></li>
<li><a href="#" target="{{ target_field }}" data="2048" class="set_speed">E1 (2.048 Mbps)</a></li>
</ul>
</span>

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load helpers %}
{% block title %}{{ provider }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -45,7 +43,7 @@
</a>
{% endif %}
</div>
<h1>{{ provider }}</h1>
<h1>{% block title %}{{ provider }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=provider %}
<div class="row">
<div class="col-md-4">

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Providers{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_provider %}
@@ -16,7 +14,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %}
</div>
<h1>Providers</h1>
<h1>{% block title %}Providers{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Console Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_consoleport %}
@@ -12,7 +10,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>Console Connections</h1>
<h1>{% block title %}Console Connections{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -21,7 +19,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Connect {{ consoleport.device }} {{ consoleport }}</div>
<div class="panel-heading">{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -21,7 +19,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Connect {{ consoleserverport.device }} {{ consoleserverport }}</div>
<div class="panel-heading">{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -196,7 +196,7 @@
{% if services %}
<table class="table table-hover panel-body">
{% for service in services %}
{% include 'dcim/inc/service.html' %}
{% include 'ipam/inc/service.html' %}
{% endfor %}
</table>
{% else %}
@@ -206,7 +206,7 @@
{% endif %}
{% if perms.ipam.add_service %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
<a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
</a>
</div>

View File

@@ -45,7 +45,7 @@
</p>
</div>
</div>
{% elif not obj.device_type.is_child_device %}
{% else %}
{% render_field form.face %}
{% render_field form.position %}
{% endif %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Devices{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_device %}
@@ -17,7 +15,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='devices' %}
</div>
<h1>Devices</h1>
<h1>{% block title %}Devices{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}

View File

@@ -80,13 +80,19 @@ $(document).ready(function() {
$('#model').html(json['get_facts']['model']);
$('#serial_number').html(json['get_facts']['serial_number']);
$('#os_version').html(json['get_facts']['os_version']);
$('#uptime').html(json['get_facts']['uptime']);
// Calculate uptime
var uptime = json['get_facts']['uptime'];
console.log(uptime);
var uptime_days = Math.floor(uptime / 86400);
var uptime_hours = Math.floor(uptime % 86400 / 3600);
var uptime_minutes = Math.floor(uptime % 3600 / 60);
$('#uptime').html(uptime_days + "d " + uptime_hours + "h " + uptime_minutes + "m");
$.each(json['get_environment']['cpu'], function(name, obj) {
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
$("#cpu").after(row)
});
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
$.each(json['get_environment']['temperature'], function(name, obj) {
var style = "success";
if (obj['is_alert']) {

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Populate {{ device_bay }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -17,7 +15,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Populate {{ device_bay }}</div>
<div class="panel-heading">{% block title %}Populate {{ device_bay }}{% endblock %}</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label required">Parent Device</label>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Device Roles{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Device Roles</h1>
<h1>{% block title %}Device Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
@@ -31,7 +29,7 @@
</div>
{% endif %}
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
<div class="row">
<div class="col-md-5">
<div class="panel panel-default">

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Device Types{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Device Types</h1>
<h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}

View File

@@ -64,7 +64,7 @@
{% endif %}
{% endif %}
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?interface_site={{ device.site.pk }}&interface_rack={{ device.rack.pk }}&interface_device={{ device.pk }}&interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Interface Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
@@ -12,7 +10,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>Interface Connections</h1>
<h1>{% block title %}Interface Connections{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Manufacturers{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_manufacturer %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Manufacturers</h1>
<h1>{% block title %}Manufacturers{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Platforms{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_platform %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Platforms</h1>
<h1>{% block title %}Platforms{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Power Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_powerport %}
@@ -12,7 +10,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>Power Connections</h1>
<h1>{% block title %}Power Connections{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -21,7 +19,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Connect {{ poweroutlet.device }} {{ poweroutlet }}</div>
<div class="panel-heading">{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -21,7 +19,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Connect {{ powerport.device }} {{ powerport }}</div>
<div class="panel-heading">{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -51,7 +49,7 @@
</a>
{% endif %}
</div>
<h1>Rack {{ rack.name }}</h1>
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=rack %}
<div class="row">
<div class="col-md-6">
@@ -261,13 +259,13 @@
<div class="rack_header">
<h4>Front</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Rear</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
</div>
</div>
</div>

View File

@@ -18,9 +18,9 @@
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
</div>
{% if face_id %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
{% else %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
{% endif %}
<div class="clearfix"></div>
<div class="rack_header">

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Racks{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rack %}
@@ -17,7 +15,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='racks' %}
</div>
<h1>Racks</h1>
<h1>{% block title %}Racks{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
@@ -27,3 +25,36 @@
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var site_list = $('#id_site');
var rack_group_list = $('#id_group_id');
// Update rack group and rack options based on selected site
site_list.change(function() {
var selected_sites = $(this).val();
if (selected_sites) {
// Update rack group options
rack_group_list.empty();
$.ajax({
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, group) {
var option = $("<option></option>").attr("value", group.id).text(group.name);
rack_group_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Groups{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackgroup %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Rack Groups</h1>
<h1>{% block title %}Rack Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Role{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackrole %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>Rack Roles</h1>
<h1>{% block title %}Rack Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Regions{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_region %}
@@ -12,7 +10,7 @@
</a>
{% endif %}
</div>
<h1>{{ block.title }}</h1>
<h1>{% block title %}Regions{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %}
{% load helpers %}
{% block title %}{{ site }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -50,7 +48,7 @@
</a>
{% endif %}
</div>
<h1>{{ site.name }}</h1>
<h1>{% block title %}{{ site }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=site %}
<div class="row">
<div class="col-md-7">

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Sites{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_site %}
@@ -16,7 +14,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='sites' %}
</div>
<h1>Sites</h1>
<h1>{% block title %}Sites{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}

View File

@@ -1,6 +1,14 @@
{% extends '_base.html' %}
{% block content %}
{% if settings.NETBOX_USERNAME or settings.NETBOX_PASSWORD %}
<div class="alert alert-warning alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong>Warning:</strong> The <code>NETBOX_USERNAME</code> and <code>NETBOX_PASSWORD</code> configuration parameters have been deprecated. Please replace them in configuration.py with <code>NAPALM_USERNAME</code> and <code>NAPALM_PASSWORD</code>.
</div>
{% endif %}
{% include 'search_form.html' %}
<div class="row">
<div class="col-sm-6 col-md-4">
@@ -47,20 +55,23 @@
</div>
</div>
</div>
{% if perms.secrets %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secrets</strong>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Virtualization</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.cluster_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:cluster_list' %}">Clusters</a></h4>
<p class="list-group-item-text text-muted">Clusters of physical hosts in which VMs reside</p>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.secret_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.virtualmachine_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></h4>
<p class="list-group-item-text text-muted">Virtual compute instances running inside clusters</p>
</div>
</div>
{% endif %}
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="panel panel-default">
@@ -112,6 +123,20 @@
</div>
</div>
</div>
{% if perms.secrets %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secrets</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.secret_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-6 col-md-4">
<div class="panel panel-default">

View File

@@ -24,7 +24,7 @@
{% empty %}
{% if table.empty_text %}
<tr>
<td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td>
<td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
</tr>
{% endif %}
{% endfor %}

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}Aggregate: {{ aggregate }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -38,7 +36,7 @@
</a>
{% endif %}
</div>
<h1>{{ aggregate }}</h1>
<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=aggregate %}
<div class="row">
<div class="col-md-6">

View File

@@ -2,8 +2,6 @@
{% load humanize %}
{% load helpers %}
{% block title %}Aggregates{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_aggregate %}
@@ -18,7 +16,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %}
</div>
<h1>Aggregates</h1>
<h1>{% block title %}Aggregates{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}

View File

@@ -14,12 +14,12 @@
<td class="text-right">
{% if perms.ipam.change_service %}
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-info btn-xs" title="Edit service">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
<i class="glyphicon glyphicon-pencil"></i>
</a>
{% endif %}
{% if perms.ipam.delete_service %}
<a href="{% url 'ipam:service_delete' pk=service.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete service"></i>
<a href="{% url 'ipam:service_delete' pk=service.pk %}?return_url={{ service.parent.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" title="Delete service"></i>
</a>
{% endif %}
</td>

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %}
{% block title %}{{ ipaddress }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -40,10 +38,10 @@
</a>
{% endif %}
</div>
<h1>{{ ipaddress }}</h1>
<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=ipaddress %}
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Address</strong>
@@ -85,7 +83,11 @@
<tr>
<td>Role</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
{% if ipaddress.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
@@ -102,7 +104,7 @@
<td>Assignment</td>
<td>
{% if ipaddress.interface %}
<span><a href="{% url 'dcim:device' pk=ipaddress.interface.device.pk %}">{{ ipaddress.interface.device }}</a> ({{ ipaddress.interface }})</span>
<span><a href="{{ ipaddress.interface.parent.get_absolute_url }}">{{ ipaddress.interface.parent }}</a> ({{ ipaddress.interface }})</span>
{% else %}
<span class="text-muted">None</span>
{% endif %}
@@ -114,7 +116,7 @@
{% if ipaddress.nat_inside %}
<a href="{% url 'ipam:ipaddress' pk=ipaddress.nat_inside.pk %}">{{ ipaddress.nat_inside }}</a>
{% if ipaddress.nat_inside.interface %}
(<a href="{% url 'dcim:device' pk=ipaddress.nat_inside.interface.device.pk %}">{{ ipaddress.nat_inside.interface.device }}</a>)
(<a href="{{ ipaddress.nat_inside.interface.parent.get_absolute_url }}">{{ ipaddress.nat_inside.interface.parent }}</a>)
{% endif %}
{% else %}
<span class="text-muted">None</span>
@@ -137,7 +139,7 @@
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div>
<div class="col-md-6">
<div class="col-md-8">
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}

View File

@@ -1,72 +0,0 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Assign an IP Address{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Assign an IP Address</strong>
</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">IP Address</label>
<div class="col-md-9">
<p class="form-control-static">{{ ipaddress }}</p>
</div>
<label class="col-md-3 control-label">VRF</label>
<div class="col-md-9">
<p class="form-control-static">
{% if ipaddress.vrf %}
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a> ({{ ipaddress.vrf.rd }})
{% else %}
<span>Global</span>
{% endif %}
</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="search">
{% render_field form.livesearch %}
</div>
<div class="tab-pane" id="select">
{% render_field form.site %}
{% render_field form.rack %}
{% render_field form.device %}
</div>
</div>
{% render_field form.interface %}
{% render_field form.set_as_primary %}
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
{% endblock %}

View File

@@ -12,7 +12,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>IP Addresses</strong></div>
<div class="panel-body">
{% render_field pattern_form.pattern %}
{% render_field form.pattern %}
{% render_field model_form.status %}
{% render_field model_form.role %}
{% render_field model_form.vrf %}

View File

@@ -1,6 +1,7 @@
{% extends 'utilities/obj_edit.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% load helpers %}
{% block tabs %}
{% if not obj.pk %}
@@ -26,18 +27,25 @@
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface Assignment</strong>
{% if obj.interface %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface Assignment</strong>
</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">{{ obj.interface.parent|model_name|bettertitle }}</label>
<div class="col-md-9">
<p class="form-control-static">
<a href="{{ obj.interface.parent.get_absolute_url }}">{{ obj.interface.parent }}</a>
</p>
</div>
</div>
{% render_field form.interface %}
{% render_field form.primary_for_parent %}
</div>
</div>
<div class="panel-body">
{% render_field form.interface_site %}
{% render_field form.interface_rack %}
{% render_field form.interface_device %}
{% render_field form.interface %}
{% render_field form.primary_for_device %}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
<div class="panel-body">

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}IP Addresses{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_ipaddress %}
@@ -17,7 +15,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='IPs' %}
</div>
<h1>IP Addresses</h1>
<h1>{% block title %}IP Addresses{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}

View File

@@ -1,8 +0,0 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Remove {{ ipaddress }} from {{ ipaddress.interface }}?{% endblock %}
{% block message %}
<p>Are you sure you want to remove this IP address from <strong>{{ ipaddress.interface.device }} {{ ipaddress.interface }}</strong>?</p>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends '_base.html' %}
{% block title %}{{ prefix }}{% endblock %}
{% block title %}{{ prefix }} - IP Addresses{% endblock %}
{% block content %}
{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}

View File

@@ -2,8 +2,6 @@
{% load helpers %}
{% load form_helpers %}
{% block title %}Prefixes{% endblock %}
{% block content %}
<div class="pull-right">
<div class="btn-group" role="group">
@@ -22,7 +20,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='prefixes' %}
</div>
<h1>Prefixes</h1>
<h1>{% block title %}Prefixes{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}

View File

@@ -2,8 +2,6 @@
{% load humanize %}
{% load helpers %}
{% block title %}RIRs{% endblock %}
{% block content %}
<div class="pull-right">
{% if request.GET.family == '6' %}
@@ -24,7 +22,7 @@
</a>
{% endif %}
</div>
<h1>RIRs</h1>
<h1>{% block title %}RIRs{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}

Some files were not shown because too many files have changed in this diff Show More