Compare commits

..

93 Commits

Author SHA1 Message Date
Jeremy Stretch
6436d703f5 Merge pull request #1852 from digitalocean/develop
Release v2.2.9
2018-01-31 10:43:20 -05:00
Jeremy Stretch
b3243704df Release v.2.2.9 2018-01-31 10:30:55 -05:00
Jeremy Stretch
8bedfcfc64 Added warning message about automatically deleting child inventory items 2018-01-31 10:25:06 -05:00
Jeremy Stretch
e0aa2c33e9 Fixes #1850: Fix TypeError when attempting IP address import if only unnamed devices exist 2018-01-31 10:03:05 -05:00
Jeremy Stretch
49f268a14c Added report results to the home page 2018-01-30 21:01:08 -05:00
Jeremy Stretch
2bb0e65aea Closes #144: Implemented list and bulk edit/delete views for InventoryItems 2018-01-30 17:46:00 -05:00
Jeremy Stretch
a5d2055c11 Closes #1073: Include prefixes/IPs from all VRFs when viewing the children of a container prefix in the global table 2018-01-30 13:39:33 -05:00
Jeremy Stretch
ffc2c564b8 Cleaned up InventoryItem add/edit/delete links and return URL 2018-01-30 13:07:10 -05:00
Jeremy Stretch
16f222b0ab Closes #1366: Enable searching for regions by name/slug 2018-01-30 12:11:20 -05:00
Jeremy Stretch
3edf90714a Closes #1406: Display tenant description as title text in object tables 2018-01-30 11:57:21 -05:00
Jeremy Stretch
4e8fc03c2b Fixes #1845: Correct display of VMs in list with no role assigned 2018-01-30 11:18:37 -05:00
Jeremy Stretch
21fe7c57d8 Closes #1835: Consistent position of previous/next rack buttons 2018-01-25 10:19:45 -05:00
Jeremy Stretch
3bcc1429dd Merge pull request #1833 from lampwins/api-docs
added statement and example for using ForeignKey ID's in write actions in api docs
2018-01-22 16:57:08 -05:00
John Anderson
6b50755a5a fixed duplicate api docs example and grammar 2018-01-22 16:26:51 -05:00
Jeremy Stretch
53998e0fff Closes #1828: Added warning about media directory permissions 2018-01-22 16:04:19 -05:00
John Anderson
7341ae087c added statement and exaple for using ForeignKey ID's in write actions 2018-01-22 10:43:19 -05:00
Jeremy Stretch
9ea8dca4e3 Evaluate device_id rather than pulling entire device (DB optimization) 2018-01-19 16:16:45 -05:00
Jeremy Stretch
5262156e1a Fixes #1818: InventoryItem API serializer no longer requires specifying a null value for items with no parent 2018-01-19 10:30:26 -05:00
Jeremy Stretch
7ac27b59c6 Closes #1824: Add virtual machine count to platforms list 2018-01-19 09:25:16 -05:00
Jeremy Stretch
d5ecfe7bef Fixes #1809: Populate tenant assignment from parent when creating a new prefix 2018-01-10 09:38:55 -05:00
Jeremy Stretch
e58d1ac87e Fixes #1807: Populate VRF from parent when creating a new prefix 2018-01-05 15:31:48 -05:00
Jeremy Stretch
bb653e733c Fixes #1621: Tweaked LLDP interface name evaluation logic 2018-01-05 15:19:27 -05:00
Jeremy Stretch
95257114df Merge pull request #1803 from bonki/doc-typo-ldap
Fixes #1802: Typo in ldap.md
2018-01-02 11:21:32 -05:00
Adrian Frühwirth
935da0d51f Fixes #1802: Typo in ldap.md 2017-12-29 13:29:07 +01:00
Jeremy Stretch
78ed85943b Fixes #1765: Improved rendering of null options for model choice fields in filter forms 2017-12-26 12:08:22 -05:00
Jeremy Stretch
b4a842d9da Post-release version bump 2017-12-20 15:32:57 -05:00
Jeremy Stretch
ec0cb7a8bc Merge pull request #1789 from digitalocean/develop
Release v2.2.8
2017-12-20 15:27:22 -05:00
Jeremy Stretch
841471104b Release v2.2.8 2017-12-20 15:24:07 -05:00
Jeremy Stretch
ac71416eb9 Closes #1775: Added instructions for enabling STARTTLS for LDAP authentication 2017-12-20 14:48:42 -05:00
Jeremy Stretch
779d685335 Closes #1784: Added cluster_type filters for virtual machines 2017-12-20 14:24:12 -05:00
Jeremy Stretch
4d1e798c56 Merge pull request #1780 from explody/fix_1778
Fix for #1778.
2017-12-20 14:17:45 -05:00
Jeremy Stretch
a598035236 Closes #1774: Include a button to refine search results for all object types under global search 2017-12-20 14:09:52 -05:00
Jeremy Stretch
50395aa821 Closes #1773: Moved child prefixes table to its own view 2017-12-20 14:01:37 -05:00
Jeremy Stretch
6d9c8fd85b Fixes #1787: Added missing site field to virtualization cluster CSV export 2017-12-20 13:18:30 -05:00
Jeremy Stretch
c3599bacf2 Fixes #1785: Omit filter forms from browsable API 2017-12-19 15:30:55 -05:00
Jeremy Stretch
c10481b99d Fixes #1783: Added vm_role filter for device roles 2017-12-19 09:37:26 -05:00
Mike Culbertson
1cebc1248b Fix for #1778.
This will set initial values for visible bulk-add form fields from query args.
2017-12-16 12:28:37 -05:00
Jeremy Stretch
c97f7041a7 Closes #1772: Added position filter for devices 2017-12-14 13:12:04 -05:00
Jeremy Stretch
89bfb4f722 Closes #1771: Added name filter for racks 2017-12-14 13:05:26 -05:00
Jeremy Stretch
da3935ff36 Fixes #1766: Fixed display of "select all" button on device power outlets list 2017-12-13 15:23:35 -05:00
Jeremy Stretch
06810bff91 Fixes #1764: Fixed typos in export buttons 2017-12-13 11:55:31 -05:00
Jeremy Stretch
a9af75bbd1 Fixes #1767: Use proper template for 404 responses 2017-12-13 11:49:36 -05:00
Jeremy Stretch
be6ef15ffa Post-release version bump 2017-12-07 14:54:16 -05:00
Jeremy Stretch
e98f0c39d1 Merge pull request #1757 from digitalocean/develop
Release v2.2.7
2017-12-07 14:52:28 -05:00
Jeremy Stretch
5666079d92 Release v2.2.7 2017-12-07 14:50:44 -05:00
Jeremy Stretch
85f5ba9a25 Fixes #1756: Improved natural ordering of console server ports and power outlets 2017-12-07 13:22:48 -05:00
Jeremy Stretch
df141a48d9 Fixed typo 2017-12-06 12:17:04 -05:00
Jeremy Stretch
fed6fc131b Fixes #1751: Corrected filtering for IPv6 addresses containing letters 2017-12-05 16:10:45 -05:00
Jeremy Stretch
cf49891853 Fixes #1740: Delete session_key cookie on logout 2017-12-05 14:19:24 -05:00
Jeremy Stretch
de2a894269 Closes #1737: Added a 'contains' API filter to find all prefixes containing a given IP or prefix 2017-11-30 12:37:41 -05:00
Jeremy Stretch
34d10f8db7 Fixes #1741: Fixed Unicode support for secret plaintexts 2017-11-29 15:16:11 -05:00
Jeremy Stretch
68f76465cf Fixes #1743: Include number of instances for device types in global search 2017-11-29 14:07:41 -05:00
Jeremy Stretch
45d6955260 Fixed search field length in search view 2017-11-28 09:27:31 -05:00
Jeremy Stretch
30df060357 Closes #1722: Added VM count to site view 2017-11-27 10:59:24 -05:00
Jeremy Stretch
252be84bf0 Corrected tenant inheritance for new IP addresses created from a parent prefix 2017-11-22 13:00:48 -05:00
Jeremy Stretch
40ab272995 Fixes #1721: Differentiated child IP count from utilization percentage for prefixes 2017-11-22 12:40:58 -05:00
Jeremy Stretch
0ec3b5db8b Closes #1722: Added virtual machine count to sites list 2017-11-22 12:19:04 -05:00
Jeremy Stretch
5dc9723585 Post-release version bump 2017-11-16 12:01:09 -05:00
Jeremy Stretch
50a451eddc Merge pull request #1720 from digitalocean/develop
Release v2.2.6
2017-11-16 12:00:34 -05:00
Jeremy Stretch
3f8350b78f Release v2.2.6 2017-11-16 11:57:43 -05:00
Jeremy Stretch
500a56b869 Fixes #1718: Set empty label to 'Global' or VRF field in IP assignment form 2017-11-16 11:54:23 -05:00
Jeremy Stretch
e50b7174bf Closes #1669: Clicking "add an IP" from the prefix view will default to the first available IP within the prefix 2017-11-15 15:26:00 -05:00
Jeremy Stretch
8299c735b1 Fixes #1599: Reduce mobile cut-off for navigation menu to 960px 2017-11-15 14:57:56 -05:00
Jeremy Stretch
124878ed22 Fixes #1599: Display global search in navigation menu unless display is less than 1200px wide 2017-11-15 14:44:33 -05:00
Jeremy Stretch
d888aa67f9 Fixes #1715: Added missing import buttons on object lists 2017-11-15 12:52:21 -05:00
Jeremy Stretch
0cb3e1749b Fixes #1717: Fixed inteface validation for virtual machines 2017-11-15 12:37:08 -05:00
Jeremy Stretch
8ff10d5995 Post-release version bump 2017-11-14 13:29:46 -05:00
Jeremy Stretch
a5a7358d26 Merge pull request #1708 from digitalocean/develop
Release v2.2.5
2017-11-14 13:25:11 -05:00
Jeremy Stretch
63ac8863f3 Release v2.2.5 2017-11-14 13:20:15 -05:00
Jeremy Stretch
2047a16a57 Fixes #1703: Added API serializer validation for custom integer fields 2017-11-14 13:15:09 -05:00
Jeremy Stretch
8d6d55d628 Fixes #1705: Fixed filtering of devices with a status of offline 2017-11-14 12:58:47 -05:00
Karl
9a7dd5ea19 Update 0008_reports.py (#1702)
* Update 0008_reports.py

PG10 version string appears to, at least on Windows, contain a comma.

* Fix missing re import.

Fix missing re import.

* Update 0008_reports.py
2017-11-13 15:11:41 -05:00
Jeremy Stretch
30b544a743 Fixes #1642: Validate device type classification when creating console server ports and power outlets 2017-11-10 15:01:46 -05:00
Jeremy Stretch
a0bb7b08bd Closes #1512: Added a view to search for an IP address being assigned to an interface 2017-11-10 11:58:59 -05:00
Jeremy Stretch
e1d655cb23 Fixes #1471: Correct bulk selection of IP addresses within a prefix assigned to a VRF 2017-11-10 09:34:30 -05:00
Jeremy Stretch
a1b1e261de Fixes #1699: Correct nested representation in the API of primary IPs for virtual machines and add missing primary_ip property 2017-11-09 09:33:40 -05:00
Jeremy Stretch
9d50b78b69 Fixes #1696: Fix for NAPALM v2.0+ 2017-11-08 09:51:37 -05:00
Jeremy Stretch
00986fd7bf Closes #1691: Cleaned up and reorganized import statements 2017-11-07 11:08:23 -05:00
Jeremy Stretch
2519ebff9d Tweaked exception-handling middleware to preserve tracebacks 2017-11-06 17:48:13 -05:00
Jeremy Stretch
c33775d71e #1689: Fix for Python 2 2017-11-06 17:44:19 -05:00
Jeremy Stretch
6b0721cc21 Fixed PermissionError handling for Python 2 2017-11-06 17:24:09 -05:00
Jeremy Stretch
d306e76420 Fixes #1689: Disregard IP address mask when filtering for child IPs of a prefix 2017-11-06 10:07:44 -05:00
Jeremy Stretch
73cd76932a Closes #1679: Added IP address roles to device/VM interface lists 2017-11-03 17:00:08 -04:00
Jeremy Stretch
5d19a9f50f Rearranged device/VM view and expanded component lists 2017-11-03 16:58:56 -04:00
Jeremy Stretch
368c30ef9d Removed unused imports 2017-11-03 14:36:28 -04:00
Jeremy Stretch
f77bf72de8 Closes #1683: Replaced default 500 handler with custom middleware to provide preliminary troubleshooting assistance 2017-11-03 13:24:31 -04:00
Jeremy Stretch
f2fbd92f78 Tweaked the issue template 2017-11-03 10:50:02 -04:00
Jeremy Stretch
480134302f Refreshed contributing docs 2017-11-03 10:10:08 -04:00
Jeremy Stretch
74cc8c022c Fixes #1650: Correct numeric ordering for interfaces with no alphabetic type 2017-11-02 13:58:30 -04:00
Jeremy Stretch
c6f3b00f0e Fixes #1676: Correct filtering of child prefixes upon bulk edit/delete from the parent prefix view 2017-11-02 13:21:19 -04:00
Jeremy Stretch
626fbd1d10 Closes #1684: Replaced prefix 'parent' filter with 'within' and 'within_include' 2017-11-02 13:15:25 -04:00
Ichabond
b8df05cf88 Fixes #1655. Removed explicit field references. (#1656)
* Fixes #1655

Further field name references were found in `consoleport.html`. These have now been removed, so we rely on proper a proper `__str__` implementation of both `ConsolePort` and `ConsoleServerPort`.

* Fixes #1655: Removed explicit field references

Cleaned up all (notable) .name references, and removed them so __str__ can do the lifting. Did not remove the references where it was explicitly referenced to .name (eg. in details). Extended the Secret model to also include the name in __str__, since that was weirdly absent.

* Adapted PR to comply with comments

Re-introduced certain references to make sure explicit references are still used where needed.
2017-11-02 11:51:27 -04:00
Jeremy Stretch
e56fc4b1ee Post-release version bump 2017-10-31 15:25:59 -04:00
153 changed files with 1557 additions and 884 deletions

View File

@@ -4,23 +4,30 @@
remove the "is:open" filter from the search bar to include closed issues. 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 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 brackets. For assistance with installation issues, or for any other issues
discussion on our mailing list: other than those listed below, please raise your topic for discussion on
our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss https://groups.google.com/forum/#!forum/netbox-discuss
Please note that issues which do not fall under any of the below categories Please note that issues which do not fall under any of the below categories
will be closed. will be closed. Due to an excessive backlog of feature requests, we are
not currently accepting any proposals which extend NetBox's feature scope.
Do not prepend any sort of tag to your issue's title. An administrator will
review your issue and assign labels as appropriate.
---> --->
### Issue type ### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature --> [ ] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report <!-- Reporting unexpected or erroneous behavior --> [ ] Bug report <!-- Unexpected or erroneous behavior -->
[ ] Documentation <!-- Proposing a modification to the documentation --> [ ] Documentation <!-- A modification to the documentation -->
<!-- <!--
Please describe the environment in which you are running NetBox. (Be sure 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 to verify that you are running the latest stable release of NetBox before
submitting a bug report.) submitting a bug report.) If you are submitting a bug report and have made
any changes to the code base, please first validate that your bug can be
recreated while running an official release.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.5.4 -->
@@ -28,8 +35,8 @@
<!-- <!--
BUG REPORTS must include: BUG REPORTS must include:
* A list of the steps needed to reproduce the bug * A list of the steps needed for someone else to reproduce the bug
* A description of the expected behavior * A description of the expected and observed behavior
* Any relevant error messages (screenshots may also help) * Any relevant error messages (screenshots may also help)
FEATURE REQUESTS must include: FEATURE REQUESTS must include:

View File

@@ -1,8 +1,8 @@
## Getting Help ## Getting Help
If you encounter any issues installing or using NetBox, try one of the If you encounter any issues installing or using NetBox, try one of the
following resources to get assistance. Please **do not** open a GitHub following resources to get assistance. Please **do not** open a GitHub issue
issue except to report bugs or request features. except to report bugs or request features.
### Mailing List ### Mailing List
@@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
### Freenode IRC ### Freenode IRC
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/). For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
You can connect to Freenode at irc.freenode.net using an IRC client, or You can connect to Freenode at irc.freenode.net using an IRC client, or you can
you can use their [webchat client](https://webchat.freenode.net/). use their [webchat client](https://webchat.freenode.net/).
## Reporting Bugs ## Reporting Bugs
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
NetBox. If you're running an older version, it's possible that the bug NetBox. If you're running an older version, it's possible that the bug has
has already been fixed. already been fixed.
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has * Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already
already been reported. If you think you may be experiencing a reported been reported. If you think you may be experiencing a reported issue that
issue that hasn't already been resolved, please click "add a reaction" hasn't already been resolved, please click "add a reaction" in the top right
in the top right corner of the issue and add a thumbs up (+1). You might corner of the issue and add a thumbs up (+1). You mightalso want to add a
also want to add a comment describing how it's affecting your comment describing how it's affecting your installation. This will allow us to
installation. This will allow us to prioritize bugs based on how many prioritize bugs based on how many users are affected.
users are affected.
* If you haven't found an existing issue that describes your suspected * If you haven't found an existing issue that describes your suspected bug,
bug, please inquire about it on the mailing list. **Do not** file an please inquire about it on the mailing list. **Do not** file an issue until you
issue until you have received confirmation that it is in fact a bug. have received confirmation that it is in fact a bug. Invalid issues are very
Invalid issues are very distracting and slow the pace at which NetBox is distracting and slow the pace at which NetBox is developed.
developed.
* When submitting an issue, please be as descriptive as possible. Be * When submitting an issue, please be as descriptive as possible. Be sure to
sure to include: include:
* The environment in which NetBox is running * The environment in which NetBox is running
* The exact steps that can be taken to reproduce the issue (if * The exact steps that can be taken to reproduce the issue (if applicable)
applicable)
* Any error messages generated * Any error messages generated
* Screenshots (if applicable) * Screenshots (if applicable)
@@ -49,71 +46,64 @@ sure to include:
The issue will be reviewed by a moderator after submission and the appropriate The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied. labels will be applied.
* Keep in mind that we prioritize bugs based on their severity and how * Keep in mind that we prioritize bugs based on their severity and how much
much work is required to resolve them. It may take some time for someone work is required to resolve them. It may take some time for someone to address
to address your issue. your issue.
## Feature Requests ## Feature Requests
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're * First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
requesting is already listed. (Be sure to search closed issues as well, is already listed. (Be sure to search closed issues as well, since some
since some feature requests are rejected.) If the feature you'd like to feature requests have been rejected.) If the feature you'd like to see has
see has already been requested, click "add a reaction" in the top right already been requested and is open, click "add a reaction" in the top right
corner of the issue and add a thumbs up (+1). This ensures that the corner of the issue and add a thumbs up (+1). This ensures that the issue has
issue has a better chance of making it onto the roadmap. Also feel free a better chance of receiving attention. Also feel free to add a comment with
to add a comment with any additional justification for the feature. any additional justification for the feature. (However, note that comments with
(However, note that comments with no substance other than a "+1" will be no substance other than a "+1" will be deleted. Please use GitHub's reactions
deleted. Please use GitHub's reactions feature to indicate your feature to indicate your support.)
support.)
* While suggestions for new features are welcome, it's important to * Due to an excessive backlog of feature requests, we are not currently
limit the scope of NetBox's feature set to avoid feature creep. For accepting any proposals which substantially extend NetBox's functionality
example, the following features would be firmly out of scope for NetBox: beyond its current feature set. This includes the introduction of any new views
or models which have not already been proposed in an existing feature request.
* Ticket management
* Network state monitoring
* Acting as a DNS server
* Acting as an authentication server
* Before filing a new feature request, consider raising your idea on the * Before filing a new feature request, consider raising your idea on the
mailing list first. Feedback you receive there will help validate and mailing list first. Feedback you receive there will help validate and shape the
shape the proposed feature before filing a formal issue. proposed feature before filing a formal issue.
* Good feature requests are very narrowly defined. Be sure to enumerate * Good feature requests are very narrowly defined. Be sure to thoroughly
specific functionality and data schema. The more effort you put into describe the functionality and data model(s) being proposed. The more effort
writing a feature request, the better its chance is of being you put into writing a feature request, the better its chance is of being
implemented. Overly broad feature requests will be closed. implemented. Overly broad feature requests will be closed.
* When submitting a feature request on GitHub, be sure to include the * When submitting a feature request on GitHub, be sure to include the
following: following:
* A detailed description of the proposed functionality * A detailed description of the proposed functionality
* A use case for the feature; who would use it and what value it * A use case for the feature; who would use it and what value it would add
would add to NetBox to NetBox
* A rough description of changes necessary to the database schema * A rough description of changes necessary to the database schema (if
(if applicable) applicable)
* Any third-party libraries or other resources which would be * Any third-party libraries or other resources which would be involved
involved
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title. * Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
The issue will be reviewed by a moderator after submission and the appropriate title. The issue will be reviewed by a moderator after submission and the
labels will be applied. appropriate labels will be applied.
## Submitting Pull Requests ## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and * Be sure to open an issue before starting work on a pull request, and discuss
discuss your idea with the NetBox maintainers before beginning work. your idea with the NetBox maintainers before beginning work. This will help
This will help prevent wasting time on something that might we might not prevent wasting time on something that might we might not be able to implement.
be able to implement. When suggesting a new feature, also make sure it When suggesting a new feature, also make sure it won't conflict with any work
won't conflict with any work that's already in progress. that's already in progress.
* When submitting a pull request, please be sure to work off of the * When submitting a pull request, please be sure to work off of the `develop`
`develop` branch, rather than `master`. In NetBox, the `develop` branch branch, rather than `master`. The `develop` branch is used for ongoing
is used for ongoing development, while `master` is used for tagging new development, while `master` is used for tagging new stable releases.
stable releases.
* All code submissions should meet the following criteria (CI will * All code submissions should meet the following criteria (CI will enforce
enforce these checks): these checks):
* Python syntax is valid * Python syntax is valid
* All tests pass when run with `./manage.py test` * All tests pass when run with `./manage.py test`

View File

@@ -82,15 +82,15 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6
### Creating a new site ### Creating a new site
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region."
``` ```
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site"}' $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site", "region": 5}'
{ {
"id": 16, "id": 16,
"name": "My New Site", "name": "My New Site",
"slug": "my-new-site", "slug": "my-new-site",
"region": null, "region": 5,
"tenant": null, "tenant": null,
"facility": "", "facility": "",
"asn": null, "asn": null,
@@ -102,6 +102,7 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
"comments": "" "comments": ""
} }
``` ```
Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action.
### Modify an existing site ### Modify an existing site
@@ -123,7 +124,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
Send an authenticated `DELETE` request to the site detail endpoint. Send an authenticated `DELETE` request to the site detail endpoint.
``` ```
$ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ $ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
* Connected to localhost (127.0.0.1) port 8000 (#0) * Connected to localhost (127.0.0.1) port 8000 (#0)
> DELETE /api/dcim/sites/16/ HTTP/1.1 > DELETE /api/dcim/sites/16/ HTTP/1.1
> User-Agent: curl/7.35.0 > User-Agent: curl/7.35.0

View File

@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
} }
``` ```
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object. When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.

View File

@@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
# Configuration # Configuration
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/).
## General Server Configuration ## General Server Configuration
@@ -52,6 +52,8 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
LDAP_IGNORE_CERT_ERRORS = True LDAP_IGNORE_CERT_ERRORS = True
``` ```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
## User Authentication ## User Authentication
!!! info !!! info
@@ -78,8 +80,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
``` ```
# User Groups for Permissions # User Groups for Permissions
!!! Info !!! info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python ```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType from django_auth_ldap.config import LDAPSearch, GroupOfNamesType

View File

@@ -88,6 +88,13 @@ Resolving deltas: 100% (1495/1495), done.
Checking connectivity... done. Checking connectivity... done.
``` ```
!!! warning
Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.)
```
# chown -R netbox:netbox /opt/netbox/netbox/media/
```
## Install Python Packages ## Install Python Packages
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.) Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)

View File

@@ -1,16 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Site from dcim.models import Site

View File

@@ -11,7 +11,6 @@ from utilities.forms import (
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField, SmallTextarea, SlugField,
) )
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -175,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')), queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')), queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),

View File

@@ -8,8 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import * from .constants import *

View File

@@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from django.utils.safestring import mark_safe from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """ CIRCUITTYPE_ACTIONS = """
{% if perms.circuit.change_circuittype %} {% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -77,7 +76,7 @@ class CircuitTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
cid = tables.LinkColumn(verbose_name='ID') cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')

View File

@@ -1,14 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site from dcim.models import Site
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.constants import GRAPH_TYPE_PROVIDER
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from extras.models import Graph
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [

View File

@@ -15,7 +15,8 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from .constants import TERM_SIDE_A, TERM_SIDE_Z
from .models import Circuit, CircuitTermination, CircuitType, Provider
# #

View File

@@ -1,19 +1,23 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from dcim.constants import (
CONNECTION_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from dcim.models import ( from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RackReservation, RackRole, Region, Site,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster from virtualization.models import Cluster
@@ -729,6 +733,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer): class WritableInventoryItemSerializer(ValidatedModelSerializer):
# Provide a default value to satisfy UniqueTogetherValidator
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
class Meta: class Meta:
model = InventoryItem model = InventoryItem

View File

@@ -1,28 +1,30 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings from dcim import filters
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
) )
from dcim import filters
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin from utilities.api import (
from .exceptions import MissingFilterException IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
)
from . import serializers from . import serializers
from .exceptions import MissingFilterException
# #
@@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
device.platform device.platform
)) ))
# Check that NAPALM is installed and verify the configured driver # Check that NAPALM is installed
try: try:
import napalm import napalm
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
except ImportError: except ImportError:
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
# TODO: Remove support for NAPALM < 2.0
try:
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
except ImportError:
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
# Validate the configured driver
try: try:
driver = napalm.get_network_driver(device.platform.napalm_driver) driver = napalm.get_network_driver(device.platform.napalm_driver)
except ModuleImportError: except ModuleImportError:

View File

@@ -1,26 +1,31 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from netaddr import EUI
from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import (
IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, STATUS_CHOICES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
)
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
) )
class RegionFilter(django_filters.FilterSet): class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@@ -36,6 +41,15 @@ class RegionFilter(django_filters.FilterSet):
model = Region model = Region
fields = ['name', 'slug'] fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
@@ -162,7 +176,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Rack model = Rack
fields = ['serial', 'type', 'width', 'u_height', 'desc_units'] fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -329,7 +343,7 @@ class DeviceRoleFilter(django_filters.FilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['name', 'slug', 'color'] fields = ['name', 'slug', 'color', 'vm_role']
class PlatformFilter(django_filters.FilterSet): class PlatformFilter(django_filters.FilterSet):
@@ -424,7 +438,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device model (slug)', label='Device model (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES choices=STATUS_CHOICES,
null_value=None
) )
is_full_depth = django_filters.BooleanFilter( is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth', name='device_type__is_full_depth',
@@ -453,7 +468,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Device model = Device
fields = ['serial'] fields = ['serial', 'position']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -598,6 +613,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)', label='Parent inventory item (ID)',
@@ -616,7 +635,19 @@ class InventoryItemFilter(DeviceComponentFilterSet):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name', 'part_id', 'serial', 'discovered'] fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(part_id__icontains=value) |
Q(serial__iexact=value) |
Q(asset_tag__iexact=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import EUI, AddrFormatError
# #

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mptt.forms import TreeNodeChoiceField
import re import re
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q from django.db.models import Count, Q
from mptt.forms import TreeNodeChoiceField
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress from ipam.models import IPAddress
@@ -19,17 +19,19 @@ from utilities.forms import (
SlugField, FilterTreeNodeMultipleChoiceField, SlugField, FilterTreeNodeMultipleChoiceField,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import (
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES,
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, STATUS_CHOICES,
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
)
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
from .models import ( from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, RackRole, Region, Site,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
) )
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = '{\d+\}'
@@ -79,6 +81,11 @@ class RegionCSVForm(forms.ModelForm):
} }
class RegionFilterForm(BootstrapMixin, forms.Form):
model = Site
q = forms.CharField(required=False, label='Search')
# #
# Sites # Sites
# #
@@ -161,7 +168,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')), queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
@@ -357,17 +364,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')), queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
label='Rack group', label='Rack group',
null_option=(0, 'None') null_label='-- None --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('racks')), queryset=Tenant.objects.annotate(filter_count=Count('racks')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=RackRole.objects.annotate(filter_count=Count('racks')), queryset=RackRole.objects.annotate(filter_count=Count('racks')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
@@ -409,7 +416,7 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')), queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
label='Rack group', label='Rack group',
null_option=(0, 'None') null_label='-- None --'
) )
@@ -1029,7 +1036,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField( rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')), queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack', label='Rack',
null_option=(0, 'None'), null_label='-- None --',
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
@@ -1038,7 +1045,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')), queryset=Tenant.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
) )
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer') manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
device_type_id = FilterChoiceField( device_type_id = FilterChoiceField(
@@ -1050,7 +1057,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
platform = FilterChoiceField( platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('devices')), queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
) )
status = forms.MultipleChoiceField(choices=device_status_choices, required=False) status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
mac_address = forms.CharField(required=False, label='MAC address') mac_address = forms.CharField(required=False, label='MAC address')
@@ -1921,3 +1928,47 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description'] fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
class InventoryItemCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found.',
}
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False,
help_text='Manufacturer name',
error_messages={
'invalid_choice': 'Invalid manufacturer.',
}
)
class Meta:
model = InventoryItem
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput)
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
part_id = forms.CharField(max_length=50, required=False, label='Part ID')
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = ['manufacturer', 'part_id', 'description']
class InventoryItemFilterForm(BootstrapMixin, forms.Form):
model = InventoryItem
q = forms.CharField(required=False, label='Search')
manufacturer = FilterChoiceField(
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
to_field_name='slug',
null_label='-- None --'
)

View File

@@ -1,12 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -15,9 +13,10 @@ from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from mptt.models import MPTTModel, TreeForeignKey
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField from utilities.fields import ColorField, NullableCharField
@@ -144,6 +143,11 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
def count_circuits(self): def count_circuits(self):
return Circuit.objects.filter(terminations__site=self).count() return Circuit.objects.filter(terminations__site=self).count()
@property
def count_vms(self):
from virtualization.models import VirtualMachine
return VirtualMachine.objects.filter(cluster__site=self).count()
# #
# Racks # Racks
@@ -1090,16 +1094,11 @@ class ConsolePort(models.Model):
class ConsoleServerPortManager(models.Manager): class ConsoleServerPortManager(models.Manager):
def get_queryset(self): def get_queryset(self):
""" # Pad any trailing digits to effect natural sorting
Include the trailing numeric portion of each port name to allow for proper ordering.
For example:
Port 1, Port 2, Port 3 ... Port 9, Port 10, Port 11 ...
Instead of:
Port 1, Port 10, Port 11 ... Port 19, Port 2, Port 20 ...
"""
return super(ConsoleServerPortManager, self).get_queryset().extra(select={ return super(ConsoleServerPortManager, self).get_queryset().extra(select={
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)", 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
}).order_by('device', 'name_as_integer') "LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded')
@python_2_unicode_compatible @python_2_unicode_compatible
@@ -1118,6 +1117,17 @@ class ConsoleServerPort(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a console server
if self.device is None:
raise ValidationError("Console server ports must be assigned to devices.")
device_type = self.device.device_type
if not device_type.is_console_server:
raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
device_type.manufacturer, device_type
))
# #
# Power ports # Power ports
@@ -1161,9 +1171,10 @@ class PowerPort(models.Model):
class PowerOutletManager(models.Manager): class PowerOutletManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting
return super(PowerOutletManager, self).get_queryset().extra(select={ return super(PowerOutletManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), " 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))", "LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')
@@ -1183,6 +1194,17 @@ class PowerOutlet(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a PDU
if self.device is None:
raise ValidationError("Power outlets must be assigned to devices.")
device_type = self.device.device_type
if not device_type.is_pdu:
raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
device_type.manufacturer, device_type
))
# #
# Interfaces # Interfaces
@@ -1239,6 +1261,14 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a network device
if self.device is not None:
device_type = self.device.device_type
if not device_type.is_network_device:
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(
device_type.manufacturer, device_type
))
# An Interface must belong to a Device *or* to a VirtualMachine # An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine: if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.") raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
@@ -1422,9 +1452,24 @@ class InventoryItem(models.Model):
discovered = models.BooleanField(default=False, verbose_name='Discovered') discovered = models.BooleanField(default=False, verbose_name='Discovered')
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
csv_headers = [
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
]
class Meta: class Meta:
ordering = ['device__id', 'parent__id', 'name'] ordering = ['device__id', 'parent__id', 'name']
unique_together = ['device', 'parent', 'name'] unique_together = ['device', 'parent', 'name']
def __str__(self): def __str__(self):
return self.name return self.name
def to_csv(self):
return csv_format([
self.device.name or '{' + self.device.pk + '}',
self.name,
self.manufacturer.name if self.manufacturer else None,
self.part_id,
self.serial,
self.asset_tag,
self.description
])

View File

@@ -44,10 +44,10 @@ class InterfaceQuerySet(QuerySet):
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')" TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)" ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)" SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)" SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)" POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)" SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)" CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)" VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"

View File

@@ -3,14 +3,14 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
) )
REGION_LINK = """ REGION_LINK = """
{% if record.get_children %} {% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i> <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
@@ -141,7 +141,7 @@ class SiteTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK) region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Site model = Site
@@ -154,11 +154,12 @@ class SiteDetailTable(SiteTable):
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs') vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits') circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
vm_count = tables.Column(accessor=Accessor('count_vms'), orderable=False, verbose_name='VMs')
class Meta(SiteTable.Meta): class Meta(SiteTable.Meta):
fields = ( fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count', 'vlan_count', 'circuit_count', 'vm_count',
) )
@@ -207,7 +208,7 @@ class RackTable(BaseTable):
name = tables.LinkColumn() name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
role = tables.TemplateColumn(RACK_ROLE) role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
@@ -231,7 +232,7 @@ class RackImportTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID') facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.TemplateColumn(template_code=COL_TENANT)
u_height = tables.Column(verbose_name='Height (U)') u_height = tables.Column(verbose_name='Height (U)')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@@ -381,13 +382,17 @@ class PlatformTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(
verbose_name='') template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions') fields = ('pk', 'name', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
# #
@@ -398,7 +403,7 @@ class DeviceTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.TemplateColumn(template_code=DEVICE_LINK) name = tables.TemplateColumn(template_code=DEVICE_LINK)
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
@@ -425,7 +430,7 @@ class DeviceDetailTable(DeviceTable):
class DeviceImportTable(BaseTable): class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.TemplateColumn(template_code=COL_TENANT)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
position = tables.Column(verbose_name='Position') position = tables.Column(verbose_name='Position')
@@ -523,3 +528,17 @@ class InterfaceConnectionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b') fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
#
# InventoryItems
#
class InventoryItemTable(BaseTable):
pk = ToggleColumn()
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
class Meta(BaseTable.Meta):
model = InventoryItem
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')

View File

@@ -1,16 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.constants import IFACE_FF_LAG, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, RackReservation, RackRole, Region, Site,
) )
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from users.models import Token from users.models import Token
@@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_console_server=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1590,7 +1590,7 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_pdu=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1667,7 +1667,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'

View File

@@ -5,9 +5,8 @@ from django.conf.urls import url
from extras.views import ImageAttachmentEditView from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView from ipam.views import ServiceCreateView
from secrets.views import secret_add from secrets.views import secret_add
from .models import Device, Rack, Site
from . import views from . import views
from .models import Device, Rack, Site
app_name = 'dcim' app_name = 'dcim'
urlpatterns = [ urlpatterns = [
@@ -196,9 +195,13 @@ urlpatterns = [
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'), url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
# Inventory items # Inventory items
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
# Console/power/interface connections # Console/power/interface connections
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from natsort import natsorted
from operator import attrgetter from operator import attrgetter
from django.contrib import messages from django.contrib import messages
@@ -15,22 +14,25 @@ from django.utils.html import escape
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from natsort import natsorted
from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
from ipam.models import Prefix, Service, VLAN
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
from .constants import CONNECTION_STATUS_CONNECTED
from .models import ( from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackGroup, RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
) )
@@ -78,6 +80,8 @@ class BulkDisconnectView(View):
class RegionListView(ObjectListView): class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites')) queryset = Region.objects.annotate(site_count=Count('sites'))
filter = filters.RegionFilter
filter_form = forms.RegionFilterForm
table = tables.RegionTable table = tables.RegionTable
template_name = 'dcim/region_list.html' template_name = 'dcim/region_list.html'
@@ -133,6 +137,7 @@ class SiteView(View):
'prefix_count': Prefix.objects.filter(site=site).count(), 'prefix_count': Prefix.objects.filter(site=site).count(),
'vlan_count': VLAN.objects.filter(site=site).count(), 'vlan_count': VLAN.objects.filter(site=site).count(),
'circuit_count': Circuit.objects.filter(terminations__site=site).count(), 'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
} }
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site) topology_maps = TopologyMap.objects.filter(site=site)
@@ -751,7 +756,10 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class PlatformListView(ObjectListView): class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate(device_count=Count('devices')) queryset = Platform.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
table = tables.PlatformTable table = tables.PlatformTable
template_name = 'dcim/platform_list.html' template_name = 'dcim/platform_list.html'
@@ -807,15 +815,11 @@ class DeviceView(View):
console_ports = natsorted( console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
) )
cs_ports = natsorted( cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
)
power_ports = natsorted( power_ports = natsorted(
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
) )
power_outlets = natsorted( power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
interfaces = Interface.objects.order_naturally( interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering device.device_type.interface_ordering
).filter( ).filter(
@@ -1811,6 +1815,14 @@ class InterfaceConnectionsListView(ObjectListView):
# Inventory items # Inventory items
# #
class InventoryItemListView(ObjectListView):
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
filter = filters.InventoryItemFilter
filter_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_list.html'
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView): class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_inventoryitem' permission_required = 'dcim.change_inventoryitem'
model = InventoryItem model = InventoryItem
@@ -1822,8 +1834,40 @@ class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
obj.device = get_object_or_404(Device, pk=url_kwargs['device']) obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
return obj return obj
def get_return_url(self, request, obj):
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView): class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_inventoryitem' permission_required = 'dcim.delete_inventoryitem'
model = InventoryItem model = InventoryItem
parent_field = 'device' parent_field = 'device'
def get_return_url(self, request, obj):
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_inventoryitem'
model_form = forms.InventoryItemCSVForm
table = tables.InventoryItemTable
default_return_url = 'dcim:inventoryitem_list'
class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_inventoryitem'
cls = InventoryItem
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
filter = filters.InventoryItemFilter
table = tables.InventoryItemTable
form = forms.InventoryItemBulkEditForm
default_return_url = 'dcim:inventoryitem_list'
class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_inventoryitem'
cls = InventoryItem
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'
default_return_url = 'dcim:inventoryitem_list'

View File

@@ -1,15 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime
from rest_framework import serializers from datetime import datetime
from rest_framework.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import transaction from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.models import ( from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue, from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
)
from utilities.api import ValidatedModelSerializer from utilities.api import ValidatedModelSerializer
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Data validation # Data validation
if value not in [None, '']: if value not in [None, '']:
# Validate integer
if cf.type == CF_TYPE_INTEGER:
try:
int(value)
except ValueError:
raise ValidationError(
"Invalid value for integer field {}: {}".format(field_name, value)
)
# Validate boolean # Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError( raise ValidationError(

View File

@@ -1,14 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers from rest_framework import serializers
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site from dcim.models import Device, Rack, Site
from extras.models import ( from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction, from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
)
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer

View File

@@ -1,13 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from extras import filters from extras import filters
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from dcim.models import Site from dcim.models import Site
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction from .constants import CF_TYPE_SELECT
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):

View File

@@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .models import ( from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue, from .models import CustomField, CustomFieldValue, ImageAttachment
ImageAttachment,
)
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):

View File

@@ -10,7 +10,6 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Model from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization'] APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
BANNER_TEXT = """### NetBox interactive shell ({node}) BANNER_TEXT = """### NetBox interactive shell ({node})

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from getpass import getpass from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from extras.models import ReportResult
from extras.reports import get_reports from extras.reports import get_reports

View File

@@ -2,6 +2,7 @@
# Generated by Django 1.11.4 on 2017-09-26 21:25 # Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals from __future__ import unicode_literals
from distutils.version import StrictVersion from distutils.version import StrictVersion
import re
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields.jsonb import django.contrib.postgres.fields.jsonb
@@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()") cursor.execute("SELECT VERSION()")
row = cursor.fetchone() row = cursor.fetchone()
pg_version = row[0].split()[1] pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'): if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version)) raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz
import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import importlib import importlib
import inspect import inspect
import pkgutil import pkgutil
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone

View File

@@ -1,11 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re import re
import time import time
from ncclient import manager
import paramiko import paramiko
import xmltodict import xmltodict
from ncclient import manager
CONNECT_TIMEOUT = 5 # seconds CONNECT_TIMEOUT = 5 # seconds

View File

@@ -1,14 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Device from dcim.models import Device
from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate from extras.constants import GRAPH_TYPE_SITE
from extras.models import Graph, ExportTemplate
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -1,19 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date
from rest_framework import status from datetime import date
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Site from dcim.models import Site
from extras.models import ( from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
CF_TYPE_SELECT, CF_TYPE_URL,
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -4,7 +4,6 @@ from django.conf.urls import url
from extras import views from extras import views
app_name = 'extras' app_name = 'extras'
urlpatterns = [ urlpatterns = [

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe

View File

@@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
@@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import ( from ipam.constants import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.api.serializers import NestedVirtualMachineSerializer from virtualization.api.serializers import NestedVirtualMachineSerializer

View File

@@ -1,17 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers

View File

@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
(VLAN_STATUS_DEPRECATED, 'Deprecated') (VLAN_STATUS_DEPRECATED, 'Deprecated')
) )
# Bootstrap CSS classes for various statuses # Bootstrap CSS classes
STATUS_CHOICE_CLASSES = { STATUS_CHOICE_CLASSES = {
0: 'default', 0: 'default',
1: 'primary', 1: 'primary',
@@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
4: 'warning', 4: 'warning',
5: 'success', 5: 'success',
} }
ROLE_CHOICE_CLASSES = {
10: 'default',
20: 'primary',
30: 'warning',
40: 'success',
41: 'success',
42: 'success',
43: 'success',
44: 'success',
}
# IP protocols (for services) # IP protocols (for services)
IP_PROTOCOL_TCP = 6 IP_PROTOCOL_TCP = 6

View File

@@ -1,15 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from netaddr import IPNetwork
from .formfields import IPFormField from .formfields import IPFormField
from .lookups import ( from . import lookups
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
NetHost, NetHostContained, NetMaskLength, Regex, StartsWith,
)
def prefix_validator(prefix): def prefix_validator(prefix):
@@ -58,17 +54,18 @@ class IPNetworkField(BaseIPField):
return 'cidr' return 'cidr'
IPNetworkField.register_lookup(EndsWith) IPNetworkField.register_lookup(lookups.IExact)
IPNetworkField.register_lookup(IEndsWith) IPNetworkField.register_lookup(lookups.EndsWith)
IPNetworkField.register_lookup(StartsWith) IPNetworkField.register_lookup(lookups.IEndsWith)
IPNetworkField.register_lookup(IStartsWith) IPNetworkField.register_lookup(lookups.StartsWith)
IPNetworkField.register_lookup(Regex) IPNetworkField.register_lookup(lookups.IStartsWith)
IPNetworkField.register_lookup(IRegex) IPNetworkField.register_lookup(lookups.Regex)
IPNetworkField.register_lookup(NetContained) IPNetworkField.register_lookup(lookups.IRegex)
IPNetworkField.register_lookup(NetContainedOrEqual) IPNetworkField.register_lookup(lookups.NetContained)
IPNetworkField.register_lookup(NetContains) IPNetworkField.register_lookup(lookups.NetContainedOrEqual)
IPNetworkField.register_lookup(NetContainsOrEquals) IPNetworkField.register_lookup(lookups.NetContains)
IPNetworkField.register_lookup(NetMaskLength) IPNetworkField.register_lookup(lookups.NetContainsOrEquals)
IPNetworkField.register_lookup(lookups.NetMaskLength)
class IPAddressField(BaseIPField): class IPAddressField(BaseIPField):
@@ -81,16 +78,17 @@ class IPAddressField(BaseIPField):
return 'inet' return 'inet'
IPAddressField.register_lookup(EndsWith) IPAddressField.register_lookup(lookups.IExact)
IPAddressField.register_lookup(IEndsWith) IPAddressField.register_lookup(lookups.EndsWith)
IPAddressField.register_lookup(StartsWith) IPAddressField.register_lookup(lookups.IEndsWith)
IPAddressField.register_lookup(IStartsWith) IPAddressField.register_lookup(lookups.StartsWith)
IPAddressField.register_lookup(Regex) IPAddressField.register_lookup(lookups.IStartsWith)
IPAddressField.register_lookup(IRegex) IPAddressField.register_lookup(lookups.Regex)
IPAddressField.register_lookup(NetContained) IPAddressField.register_lookup(lookups.IRegex)
IPAddressField.register_lookup(NetContainedOrEqual) IPAddressField.register_lookup(lookups.NetContained)
IPAddressField.register_lookup(NetContains) IPAddressField.register_lookup(lookups.NetContainedOrEqual)
IPAddressField.register_lookup(NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetContains)
IPAddressField.register_lookup(NetHost) IPAddressField.register_lookup(lookups.NetContainsOrEquals)
IPAddressField.register_lookup(NetHostContained) IPAddressField.register_lookup(lookups.NetHost)
IPAddressField.register_lookup(NetMaskLength) IPAddressField.register_lookup(lookups.NetHostContained)
IPAddressField.register_lookup(lookups.NetMaskLength)

View File

@@ -1,20 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from netaddr import IPNetwork
from netaddr.core import AddrFormatError
from django.db.models import Q from django.db.models import Q
import netaddr
from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import ( from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -82,7 +79,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = Q(description__icontains=value)
try: try:
prefix = str(IPNetwork(value.strip()).cidr) prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix) qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
pass pass
@@ -102,9 +99,22 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
# TODO: Deprecate in v2.3.0
parent = django_filters.CharFilter( parent = django_filters.CharFilter(
method='search_by_parent', method='search_within_include',
label='Parent prefix', label='Parent prefix (deprecated)',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
)
within_include = django_filters.CharFilter(
method='search_within_include',
label='Within and including prefix',
)
contains = django_filters.CharFilter(
method='search_contains',
label='Prefixes which contain this prefix or IP',
) )
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
@@ -159,7 +169,8 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=PREFIX_STATUS_CHOICES choices=PREFIX_STATUS_CHOICES,
null_value=None
) )
class Meta: class Meta:
@@ -171,22 +182,46 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = Q(description__icontains=value)
try: try:
prefix = str(IPNetwork(value.strip()).cidr) prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix) qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
pass pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value): def search_within(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:
return queryset return queryset
try: try:
query = str(IPNetwork(value).cidr) query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
def search_within_include(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained_or_equal=query) return queryset.filter(prefix__net_contained_or_equal=query)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
def search_contains(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
# Searching by prefix
if '/' in value:
return queryset.filter(prefix__net_contains_or_equals=str(netaddr.IPNetwork(value).cidr))
# Searching by IP address
else:
return queryset.filter(prefix__net_contains=str(netaddr.IPAddress(value)))
except (AddrFormatError, ValueError):
return queryset.none()
def filter_mask_length(self, queryset, name, value): def filter_mask_length(self, queryset, name, value):
if not value: if not value:
return queryset return queryset
@@ -254,7 +289,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES choices=IPADDRESS_STATUS_CHOICES,
null_value=None
) )
role = django_filters.MultipleChoiceFilter( role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES choices=IPADDRESS_ROLE_CHOICES
@@ -278,7 +314,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
query = str(IPNetwork(value.strip()).cidr) query = str(netaddr.IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query) return queryset.filter(address__net_host_contained=query)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
@@ -353,7 +389,8 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VLAN_STATUS_CHOICES choices=VLAN_STATUS_CHOICES,
null_value=None
) )
class Meta: class Meta:

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import IPNetwork, AddrFormatError
# #

View File

@@ -14,11 +14,8 @@ from utilities.forms import (
add_blank_choice, add_blank_choice,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import ( from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
IP_FAMILY_CHOICES = [ IP_FAMILY_CHOICES = [
('', 'All'), ('', 'All'),
@@ -81,8 +78,11 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VRF model = VRF
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug', tenant = FilterChoiceField(
null_option=(0, None)) queryset=Tenant.objects.annotate(filter_count=Count('vrfs')),
to_field_name='slug',
null_label='-- None --'
)
# #
@@ -362,7 +362,7 @@ def prefix_status_choices():
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix model = Prefix
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={ within_include = forms.CharField(required=False, label='Search within', widget=forms.TextInput(attrs={
'placeholder': 'Prefix', 'placeholder': 'Prefix',
})) }))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
@@ -371,23 +371,23 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=VRF.objects.annotate(filter_count=Count('prefixes')), queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_option=(0, 'Global') null_label='-- Global --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False) status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('prefixes')), queryset=Site.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate(filter_count=Count('prefixes')), queryset=Role.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy') expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
@@ -691,6 +691,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['vrf', 'role', 'tenant', 'description'] nullable_fields = ['vrf', 'role', 'tenant', 'description']
class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
address = forms.CharField(label='IP Address')
def ipaddress_status_choices(): def ipaddress_status_choices():
status_counts = {} status_counts = {}
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
@@ -717,12 +722,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_option=(0, 'Global') null_label='-- Global --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')), queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False) role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
@@ -764,7 +769,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'Global') null_label='-- Global --'
) )
@@ -894,23 +899,23 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('vlans')), queryset=Site.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'Global') null_label='-- Global --'
) )
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
label='VLAN group', label='VLAN group',
null_option=(0, 'None') null_label='-- None --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('vlans')), queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False) status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate(filter_count=Count('vlans')), queryset=Role.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )

View File

@@ -13,12 +13,21 @@ class NetFieldDecoratorMixin(object):
return lhs_string, lhs_params return lhs_string, lhs_params
class IExact(NetFieldDecoratorMixin, lookups.IExact):
def get_rhs_op(self, connection, rhs):
return '= LOWER(%s)' % rhs
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith): class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
lookup_name = 'endswith' pass
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith): class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
lookup_name = 'iendswith' pass
def get_rhs_op(self, connection, rhs):
return 'LIKE LOWER(%s)' % rhs
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith): class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
@@ -26,15 +35,18 @@ class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith): class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
lookup_name = 'istartswith' pass
def get_rhs_op(self, connection, rhs):
return 'LIKE LOWER(%s)' % rhs
class Regex(NetFieldDecoratorMixin, lookups.Regex): class Regex(NetFieldDecoratorMixin, lookups.Regex):
lookup_name = 'regex' pass
class IRegex(NetFieldDecoratorMixin, lookups.IRegex): class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
lookup_name = 'iregex' pass
class NetContainsOrEquals(Lookup): class NetContainsOrEquals(Lookup):

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr
import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@@ -282,11 +281,35 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_duplicates(self): def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_child_prefixes(self):
"""
Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child
Prefixes belonging to any VRF.
"""
if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
return Prefix.objects.filter(prefix__net_contained=str(self.prefix))
else:
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
def get_child_ips(self): def get_child_ips(self):
""" """
Return all IPAddresses within this Prefix. Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
child IPAddresses belonging to any VRF.
""" """
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf) if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix))
else:
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
def get_available_prefixes(self):
"""
Return all available Prefixes within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
available_prefixes = prefix - child_prefixes
return available_prefixes
def get_available_ips(self): def get_available_ips(self):
""" """
@@ -305,6 +328,24 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return available_ips return available_ips
def get_first_available_prefix(self):
"""
Return the first available child prefix within the prefix (or None).
"""
available_prefixes = self.get_available_prefixes()
if not available_prefixes:
return None
return available_prefixes.iter_cidrs()[0]
def get_first_available_ip(self):
"""
Return the first available IP within the prefix (or None).
"""
available_ips = self.get_available_ips()
if not available_ips:
return None
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
@@ -315,25 +356,12 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
else: else:
child_count = IPAddress.objects.filter( child_count = self.get_child_ips().count()
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
).count()
prefix_size = self.prefix.size prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2 prefix_size -= 2
return int(float(child_count) / prefix_size * 100) return int(float(child_count) / prefix_size * 100)
@property
def new_subnet(self):
if self.family == 4:
if self.prefix.prefixlen <= 30:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
if self.family == 6:
if self.prefix.prefixlen <= 126:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
class IPAddressManager(models.Manager): class IPAddressManager(models.Manager):
@@ -461,6 +489,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
def get_role_class(self):
return ROLE_CHOICE_CLASSES[self.role]
@python_2_unicode_compatible @python_2_unicode_compatible
class VLANGroup(models.Model): class VLANGroup(models.Model):

View File

@@ -3,10 +3,10 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """ RIR_UTILIZATION = """
<div class="progress"> <div class="progress">
{% if record.stats.total %} {% if record.stats.total %}
@@ -49,13 +49,7 @@ PREFIX_LINK = """
{% else %} {% else %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px"> <span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
{% endif %} {% 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> <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 %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span>
"""
PREFIX_LINK_BRIEF = """
<span style="padding-left: {{ record.depth }}0px">
<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> </span>
""" """
@@ -77,6 +71,10 @@ IPADDRESS_LINK = """
{% endif %} {% endif %}
""" """
IPADDRESS_ASSIGN_LINK = """
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
"""
IPADDRESS_PARENT = """ IPADDRESS_PARENT = """
{% if record.interface %} {% if record.interface %}
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a> <a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
@@ -134,9 +132,9 @@ VLANGROUP_ACTIONS = """
TENANT_LINK = """ TENANT_LINK = """
{% if record.tenant %} {% if record.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a> <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
{% elif record.vrf.tenant %} {% elif record.vrf.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>* <a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
{% else %} {% else %}
&mdash; &mdash;
{% endif %} {% endif %}
@@ -151,7 +149,7 @@ class VRFTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
rd = tables.Column(verbose_name='RD') rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VRF model = VRF
@@ -242,7 +240,7 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(template_code=TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
role = tables.TemplateColumn(PREFIX_ROLE_LINK) role = tables.TemplateColumn(PREFIX_ROLE_LINK)
@@ -269,9 +267,9 @@ class PrefixDetailTable(PrefixTable):
class IPAddressTable(BaseTable): class IPAddressTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK) status = tables.TemplateColumn(STATUS_LABEL)
tenant = tables.TemplateColumn(template_code=TENANT_LINK)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False) interface = tables.Column(orderable=False)
@@ -294,6 +292,18 @@ class IPAddressDetailTable(IPAddressTable):
) )
class IPAddressAssignTable(BaseTable):
address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
orderable = False
# #
# VLAN groups # VLAN groups
# #
@@ -321,7 +331,7 @@ class VLANTable(BaseTable):
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK) role = tables.TemplateColumn(VLAN_ROLE_LINK)

View File

@@ -1,16 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.urls import reverse
from netaddr import IPNetwork from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.models import ( from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF, from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr import netaddr
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings

View File

@@ -51,6 +51,7 @@ urlpatterns = [
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses # IP addresses
@@ -60,6 +61,7 @@ urlpatterns = [
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),

View File

@@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django_tables2 import RequestConfig
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig
from dcim.models import Device, Interface from dcim.models import Device, Interface
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
@@ -17,11 +16,8 @@ from utilities.views import (
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
from .models import ( from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
)
def add_available_prefixes(parent, prefix_list): def add_available_prefixes(parent, prefix_list):
@@ -458,11 +454,6 @@ class PrefixView(View):
except Aggregate.DoesNotExist: except Aggregate.DoesNotExist:
aggregate = None aggregate = None
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).count()
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter( parent_prefixes = Prefix.objects.filter(
Q(vrf=prefix.vrf) | Q(vrf__isnull=True) Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
@@ -485,23 +476,38 @@ class PrefixView(View):
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False) duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table.exclude = ('vrf',) duplicate_prefix_table.exclude = ('vrf',)
return render(request, 'ipam/prefix.html', {
'prefix': prefix,
'aggregate': aggregate,
'parent_prefix_table': parent_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
})
class PrefixPrefixesView(View):
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Child prefixes table # Child prefixes table
child_prefixes = Prefix.objects.filter( child_prefixes = prefix.get_child_prefixes().select_related(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related(
'site', 'vlan', 'role', 'site', 'vlan', 'role',
).annotate_depth(limit=0) ).annotate_depth(limit=0)
# Annotate available prefixes
if child_prefixes: if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.columns.show('pk') prefix_table.columns.show('pk')
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
} }
RequestConfig(request, paginate).configure(child_prefix_table) RequestConfig(request, paginate).configure(prefix_table)
# Compile permissions list for rendering the object table # Compile permissions list for rendering the object table
permissions = { permissions = {
@@ -510,15 +516,12 @@ class PrefixView(View):
'delete': request.user.has_perm('ipam.delete_prefix'), 'delete': request.user.has_perm('ipam.delete_prefix'),
} }
return render(request, 'ipam/prefix.html', { return render(request, 'ipam/prefix_prefixes.html', {
'prefix': prefix, 'prefix': prefix,
'aggregate': aggregate, 'first_available_prefix': prefix.get_first_available_prefix(),
'ipaddress_count': ipaddress_count, 'prefix_table': prefix_table,
'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
'permissions': permissions, 'permissions': permissions,
'return_url': prefix.get_absolute_url(), 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
}) })
@@ -529,9 +532,7 @@ class PrefixIPAddressesView(View):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk) prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix # Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter( ipaddresses = prefix.get_child_ips().select_related(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).select_related(
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for' 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
) )
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
@@ -555,9 +556,10 @@ class PrefixIPAddressesView(View):
return render(request, 'ipam/prefix_ipaddresses.html', { return render(request, 'ipam/prefix_ipaddresses.html', {
'prefix': prefix, 'prefix': prefix,
'first_available_ip': prefix.get_first_available_ip(),
'ip_table': ip_table, 'ip_table': ip_table,
'permissions': permissions, 'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
}) })
@@ -693,6 +695,51 @@ class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
class IPAddressAssignView(PermissionRequiredMixin, View):
"""
Search for IPAddresses to be assigned to an Interface.
"""
permission_required = 'ipam.change_ipaddress'
def dispatch(self, request, *args, **kwargs):
# Redirect user if an interface has not been provided
if 'interface' not in request.GET:
return redirect('ipam:ipaddress_add')
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
def get(self, request):
form = forms.IPAddressAssignForm()
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'return_url': request.GET.get('return_url', ''),
})
def post(self, request):
form = forms.IPAddressAssignForm(request.POST)
table = None
if form.is_valid():
queryset = IPAddress.objects.select_related(
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
).filter(
vrf=form.cleaned_data['vrf'],
address__net_host=form.cleaned_data['address'],
)
table = tables.IPAddressAssignTable(queryset)
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'table': table,
'return_url': request.GET.get('return_url', ''),
})
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
model = IPAddress model = IPAddress

View File

@@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
def show_form_for_method(self, *args, **kwargs): def show_form_for_method(self, *args, **kwargs):
return False return False
def get_filter_form(self, data, view, request):
return None
# #
# Authentication # Authentication

View File

@@ -4,7 +4,6 @@ from django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
OBJ_TYPE_CHOICES = ( OBJ_TYPE_CHOICES = (
('', 'All Objects'), ('', 'All Objects'),
('Circuits', ( ('Circuits', (
@@ -39,7 +38,7 @@ OBJ_TYPE_CHOICES = (
class SearchForm(BootstrapMixin, forms.Form): class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField( q = forms.CharField(
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'}) label='Search'
) )
obj_type = forms.ChoiceField( obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type' choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

@@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.2.4' VERSION = '2.2.9'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -148,6 +148,7 @@ MIDDLEWARE = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'utilities.middleware.ExceptionHandlingMiddleware',
'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.LoginRequiredMiddleware',
'utilities.middleware.APIVersionMiddleware', 'utilities.middleware.APIVersionMiddleware',
) )

View File

@@ -1,17 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework_swagger.views import get_swagger_view
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.static import serve from django.views.static import serve
from rest_framework_swagger.views import get_swagger_view
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500 from netbox.views import APIRootView, HomeView, SearchView
from users.views import LoginView, LogoutView from users.views import LoginView, LogoutView
handler500 = handle_500
swagger_view = get_swagger_view(title='NetBox API') swagger_view = get_swagger_view(title='NetBox API')
_patterns = [ _patterns = [
@@ -48,9 +45,6 @@ _patterns = [
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
# Error testing
url(r'^500/$', trigger_500),
# Admin # Admin
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),

View File

@@ -1,13 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
import sys
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.reverse import reverse
from django.db.models import Count
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from circuits.filters import CircuitFilter, ProviderFilter from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider from circuits.models import Circuit, Provider
@@ -15,7 +15,7 @@ from circuits.tables import CircuitTable, ProviderTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
from extras.models import TopologyMap, UserAction from extras.models import ReportResult, TopologyMap, UserAction
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
@@ -30,7 +30,6 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineDetailTable from virtualization.tables import ClusterTable, VirtualMachineDetailTable
from .forms import SearchForm from .forms import SearchForm
SEARCH_MAX_RESULTS = 15 SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict(( SEARCH_TYPES = OrderedDict((
# Circuits # Circuits
@@ -60,7 +59,7 @@ SEARCH_TYPES = OrderedDict((
'url': 'dcim:rack_list', 'url': 'dcim:rack_list',
}), }),
('devicetype', { ('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'), 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
'filter': DeviceTypeFilter, 'filter': DeviceTypeFilter,
'table': DeviceTypeTable, 'table': DeviceTypeTable,
'url': 'dcim:devicetype_list', 'url': 'dcim:devicetype_list',
@@ -178,6 +177,7 @@ class HomeView(View):
'search_form': SearchForm(), 'search_form': SearchForm(),
'stats': stats, 'stats': stats,
'topology_maps': TopologyMap.objects.filter(site__isnull=True), 'topology_maps': TopologyMap.objects.filter(site__isnull=True),
'report_results': ReportResult.objects.order_by('-created')[:10],
'recent_activity': UserAction.objects.select_related('user')[:50] 'recent_activity': UserAction.objects.select_related('user')[:50]
}) })
@@ -247,23 +247,3 @@ class APIRootView(APIView):
('tenancy', reverse('tenancy-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)), ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
))) )))
def handle_500(request):
"""
Custom server error handler
"""
type_, error, traceback = sys.exc_info()
return render(request, '500.html', {
'exception': str(type_),
'error': error,
}, status=500)
def trigger_500(request):
"""
Hot-wired method of triggering a server error to test reporting
"""
raise Exception(
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
)

View File

@@ -1,12 +1,3 @@
"""
WSGI config for do_ipam project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application

View File

@@ -35,8 +35,22 @@ footer p {
margin: 20px 0; margin: 20px 0;
} }
/* Collapse the nav menu on displays less than 1200px wide */ /* Hide the username in the navigation menu on displays less than 1400px wide */
@media (max-width: 1399px) {
#navbar_user {
display: none;
}
}
/* Hide the search bar in the navigation menu on displays less than 1200px wide */
@media (max-width: 1199px) { @media (max-width: 1199px) {
#navbar_search {
display: none;
}
}
/* Collapse the nav menu on displays less than 960px wide */
@media (max-width: 959px) {
.navbar-header { .navbar-header {
float: none; float: none;
} }
@@ -72,12 +86,8 @@ footer p {
.collapse.in { .collapse.in {
display:block !important; display:block !important;
} }
} #navbar_user {
display: inline;
/* Hide the nav search bar on displays less than 1600px wide */
@media (max-width: 1599px) {
#navbar_search {
display: none;
} }
} }
@@ -395,6 +405,9 @@ table.reports td.stats label {
} }
/* Misc */ /* Misc */
.text-nowrap {
white-space: nowrap;
}
.banner-bottom { .banner-bottom {
margin-bottom: 50px; margin-bottom: 50px;
} }

View File

@@ -1,21 +1,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.http import HttpResponseBadRequest
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ViewSet from rest_framework.viewsets import ModelViewSet, ViewSet
from django.http import HttpResponseBadRequest
from secrets import filters from secrets import filters
from secrets.exceptions import InvalidKey from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers
ERR_USERKEY_MISSING = "No UserKey found for the current user." ERR_USERKEY_MISSING = "No UserKey found for the current user."
ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption." ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
ERR_PRIVKEY_MISSING = "Private key was not provided." ERR_PRIVKEY_MISSING = "Private key was not provided."

View File

@@ -1,12 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device from dcim.models import Device
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
from .models import Secret, SecretRole
class SecretRoleFilter(django_filters.FilterSet): class SecretRoleFilter(django_filters.FilterSet):

View File

@@ -2,7 +2,6 @@ from __future__ import unicode_literals
from Crypto.Cipher import PKCS1_OAEP from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count

View File

@@ -1,10 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Util import strxor from Crypto.Util import strxor
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
@@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
super(Secret, self).__init__(*args, **kwargs) super(Secret, self).__init__(*args, **kwargs)
def __str__(self): def __str__(self):
if self.role and self.device and self.name:
return '{} for {} ({})'.format(self.role, self.device, self.name)
# Return role and device if no name is set
if self.role and self.device: if self.role and self.device:
return '{} for {}'.format(self.role, self.device) return '{} for {}'.format(self.role, self.device)
return 'Secret' return 'Secret'
@@ -300,6 +303,7 @@ class Secret(CreatedUpdatedModel):
|LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
+--+--------+-------------------------------------------+ +--+--------+-------------------------------------------+
""" """
s = s.encode('utf8')
if len(s) > 65535: if len(s) > 65535:
raise ValueError("Maximum plaintext size is 65535 bytes.") raise ValueError("Maximum plaintext size is 65535 bytes.")
# Minimum ciphertext size is 64 bytes to conceal the length of short secrets. # Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
@@ -312,7 +316,7 @@ class Secret(CreatedUpdatedModel):
return ( return (
chr(len(s) >> 8).encode() + chr(len(s) >> 8).encode() +
chr(len(s) % 256).encode() + chr(len(s) % 256).encode() +
s.encode() + s +
os.urandom(pad_length) os.urandom(pad_length)
) )
@@ -321,11 +325,11 @@ class Secret(CreatedUpdatedModel):
Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the
plaintext. plaintext.
""" """
if isinstance(s[0], int): if isinstance(s[0], str):
plaintext_length = (s[0] << 8) + s[1]
elif isinstance(s[0], str):
plaintext_length = (ord(s[0]) << 8) + ord(s[1]) plaintext_length = (ord(s[0]) << 8) + ord(s[1])
return s[2:plaintext_length + 2].decode() else:
plaintext_length = (s[0] << 8) + s[1]
return s[2:plaintext_length + 2].decode('utf8')
def encrypt(self, secret_key): def encrypt(self, secret_key):
""" """

View File

@@ -3,10 +3,8 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import SecretRole, Secret from .models import SecretRole, Secret
SECRETROLE_ACTIONS = """ SECRETROLE_ACTIONS = """
{% if perms.secrets.change_secretrole %} {% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

View File

@@ -1,18 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64
from rest_framework import status import base64
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin
# Dummy RSA key pair for testing use only # Dummy RSA key pair for testing use only
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z

View File

@@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
from secrets.hashers import SecretValidationHasher from secrets.hashers import SecretValidationHasher
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
class UserKeyTestCase(TestCase): class UserKeyTestCase(TestCase):

View File

@@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'secrets' app_name = 'secrets'
urlpatterns = [ urlpatterns = [

View File

@@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
from django.contrib import messages from django.contrib import messages
@@ -165,7 +166,7 @@ def secret_edit(request, pk):
# Create and encrypt the new Secret # Create and encrypt the new Secret
if master_key is not None: if master_key is not None:
secret = form.save(commit=False) secret = form.save(commit=False)
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = form.cleaned_data['plaintext']
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() secret.save()
messages.success(request, "Modified secret {}.".format(secret)) messages.success(request, "Modified secret {}.".format(secret))

View File

@@ -12,7 +12,7 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-6 col-md-offset-3">
<div class="panel panel-danger" style="margin-top: 200px"> <div class="panel panel-danger" style="margin-top: 200px">
<div class="panel-heading"> <div class="panel-heading">
<strong> <strong>
@@ -21,13 +21,20 @@
</strong> </strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>There was a problem with your request. This error has been logged and administrative staff have {% block message %}
been notified. Please return to the home page and try again.</p> <p>
<p>If you are responsible for this installation, please consider There was a problem with your request. Please contact an administrator.
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional </p>
information is provided below:</p> {% endblock %}
<hr />
<p>
The complete exception is provided below:
</p>
<pre><strong>{{ exception }}</strong><br /> <pre><strong>{{ exception }}</strong><br />
{{ error }}</pre> {{ error }}</pre>
<p>
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
</p>
<div class="text-right"> <div class="text-right">
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a> <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
</div> </div>

View File

@@ -65,7 +65,7 @@
<td> <td>
{% if circuit.tenant %} {% if circuit.tenant %}
{% if circuit.tenant.group %} {% if circuit.tenant.group %}
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a> <a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a> <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit type Add a circuit type
</a> </a>
<a href="{% url 'circuits:circuittype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuit types
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Circuit Types{% endblock %}</h1> <h1>{% block title %}Circuit Types{% endblock %}</h1>

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='info' %} {% include 'dcim/inc/device_header.html' with active_tab='info' %}
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Device</strong> <strong>Device</strong>
@@ -28,10 +28,10 @@
<td> <td>
{% if device.rack %} {% if device.rack %}
{% if device.rack.group %} {% if device.rack.group %}
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a> <a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %} <a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@@ -42,7 +42,7 @@
<td> <td>
{% if device.parent_bay %} {% if device.parent_bay %}
{% with device.parent_bay.device as parent %} {% with device.parent_bay.device as parent %}
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }} <a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay }}
{% if parent.position %} {% if parent.position %}
(U{{ parent.position }} / {{ parent.get_face_display }}) (U{{ parent.position }} / {{ parent.get_face_display }})
{% endif %} {% endif %}
@@ -61,7 +61,7 @@
<td> <td>
{% if device.tenant %} {% if device.tenant %}
{% if device.tenant.group %} {% if device.tenant.group %}
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a> <a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a> <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
@@ -172,6 +172,69 @@
{% with device.get_custom_fields as custom_fields %} {% with device.get_custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %} {% include 'inc/custom_fields_panel.html' %}
{% endwith %} {% endwith %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console / Power</strong>
</div>
<table class="table table-hover panel-body component-list">
{% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %}
{% empty %}
{% if device.device_type.console_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No console ports defined
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for pp in power_ports %}
{% include 'dcim/inc/powerport.html' %}
{% empty %}
{% if device.device_type.power_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No power ports defined
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right">
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a>
{% endif %}
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a>
{% endif %}
</div>
{% endif %}
</div>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@@ -224,67 +287,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console / Power</strong>
</div>
<table class="table table-hover panel-body component-list">
{% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %}
{% empty %}
{% if device.device_type.console_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No console ports defined
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for pp in power_ports %}
{% include 'dcim/inc/powerport.html' %}
{% empty %}
{% if device.device_type.power_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No power ports defined
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right">
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a>
{% endif %}
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a>
{% endif %}
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Images</strong> <strong>Images</strong>
@@ -326,7 +328,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-7"> </div>
<div class="row">
<div class="col-md-12">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} {% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}"> <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@@ -350,7 +354,7 @@
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
{% for devicebay in device_bays %} {% for devicebay in device_bays %}
{% include 'dcim/inc/devicebay.html' with selectable=True %} {% include 'dcim/inc/devicebay.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No device bays defined</td> <td colspan="4">No device bays defined</td>
@@ -405,11 +409,23 @@
</div> </div>
</div> </div>
<table id="interfaces_table" class="table table-hover panel-body component-list"> <table id="interfaces_table" class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<th></th>
{% endif %}
<th>Name</th>
<th>LAG</th>
<th>Description</th>
<th>MTU</th>
<th>MAC Address</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for iface in interfaces %} {% for iface in interfaces %}
{% include 'dcim/inc/interface.html' with selectable=True %} {% include 'dcim/inc/interface.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No interfaces defined</td> <td colspan="8">No interfaces defined</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
@@ -467,8 +483,16 @@
</div> </div>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<th></th>
{% endif %}
<th>Name</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for csp in cs_ports %} {% for csp in cs_ports %}
{% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% include 'dcim/inc/consoleserverport.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No console server ports defined</td> <td colspan="4">No console server ports defined</td>
@@ -511,7 +535,7 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Power Outlets</strong> <strong>Power Outlets</strong>
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %} {% if perms.dcim.change_poweroutlet and power_outlets|length > 1 %}
<button class="btn btn-default btn-xs toggle"> <button class="btn btn-default btn-xs toggle">
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
</button> </button>
@@ -524,12 +548,20 @@
</div> </div>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<th></th>
{% endif %}
<th>Name</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for po in power_outlets %} {% for po in power_outlets %}
{% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% include 'dcim/inc/poweroutlet.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No power outlets defined</td> <td colspan="4">No power outlets defined</td>
</tr> </tr> text-nowrap
{% endfor %} {% endfor %}
</table> </table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %} {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}

View File

@@ -64,13 +64,14 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-success"> <div class="panel-footer text-right">
<span class="fa fa-plus" aria-hidden="true"></span> <a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs">
Add Inventory Item <span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
</a> </a>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -58,9 +58,10 @@ $(document).ready(function() {
// Glean configured hostnames/interfaces from the DOM // Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data'); var configured_device = row.children('td.configured_device').attr('data');
var configured_interface = row.children('td.configured_interface').attr('data'); var configured_interface = row.children('td.configured_interface').attr('data');
var configured_interface_short = null;
if (configured_interface) { if (configured_interface) {
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1). // Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2"); configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
} }
// Clean up hostnames/interfaces learned via LLDP // Clean up hostnames/interfaces learned via LLDP
@@ -76,6 +77,8 @@ $(document).ready(function() {
row.addClass('info'); row.addClass('info');
} else if (configured_device == lldp_device && configured_interface == lldp_interface) { } else if (configured_device == lldp_device && configured_interface == lldp_interface) {
row.addClass('success'); row.addClass('success');
} else if (configured_device == lldp_device && configured_interface_short == lldp_interface) {
row.addClass('success');
} else { } else {
row.addClass('danger'); row.addClass('danger');
} }

View File

@@ -26,7 +26,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label required">Bay</label> <label class="col-md-3 control-label required">Bay</label>
<div class="col-md-9"> <div class="col-md-9">
<p class="form-control-static">{{ device_bay.name }}</p> <p class="form-control-static">{{ device_bay }}</p>
</div> </div>
</div> </div>
{% render_form form %} {% render_form form %}

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a device role Add a device role
</a> </a>
<a href="{% url 'dcim:devicerole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import device roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Device Roles{% endblock %}</h1> <h1>{% block title %}Device Roles{% endblock %}</h1>

View File

@@ -1,13 +1,13 @@
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}"> <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
</td> </td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
</td> </td>
<td> <td>
{{ cp.cs_port.name }} {{ cp.cs_port }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">

View File

@@ -1,5 +1,5 @@
<tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}"> <tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ csp.pk }}" /> <input name="pk" type="checkbox" value="{{ csp.pk }}" />
</td> </td>
@@ -7,20 +7,19 @@
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }} <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
</td> </td>
<td></td>
{% if csp.connected_console %} {% if csp.connected_console %}
<td> <td>
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a> <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
</td> </td>
<td> <td>
{{ csp.connected_console.name }} {{ csp.connected_console }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_consoleserverport %} {% if perms.dcim.change_consoleserverport %}
{% if csp.connected_console %} {% if csp.connected_console %}
{% if csp.connected_console.connection_status %} {% if csp.connected_console.connection_status %}

View File

@@ -8,7 +8,7 @@
{% endif %} {% endif %}
{% if device.parent_bay %} {% if device.parent_bay %}
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li> <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
<li>{{ device.parent_bay.name }}</li> <li>{{ device.parent_bay }}</li>
{% endif %} {% endif %}
<li>{{ device }}</li> <li>{{ device }}</li>
</ol> </ol>

View File

@@ -1,11 +1,11 @@
<tr class="devicebay"> <tr class="devicebay">
{% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" /> <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }} <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay }}
</td> </td>
{% if devicebay.installed_device %} {% if devicebay.installed_device %}
<td> <td>

View File

@@ -1,21 +1,37 @@
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}"> <tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
{# Checkbox #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td> </td>
{% endif %} {% endif %}
{# Icon and name #}
<td>
<span title="{{ iface.get_form_factor_display }}">
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
{{ iface }}
</span>
</td>
{# LAG #}
<td> <td>
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.lag %} {% if iface.lag %}
<span class="label label-primary">{{ iface.lag.name }}</span> <a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
{% endif %}
{% if iface.description %}
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %} {% endif %}
</td> </td>
{# Description #}
<td>{{ iface.description }}</td>
{# MTU #}
<td>{{ iface.mtu|default:"" }}</td> <td>{{ iface.mtu|default:"" }}</td>
{# MAC address #}
<td>{{ iface.mac_address|default:"" }}</td> <td>{{ iface.mac_address|default:"" }}</td>
{# Connection or type #}
{% if iface.is_lag %} {% if iface.is_lag %}
<td colspan="2" class="text-muted"> <td colspan="2" class="text-muted">
LAG interface<br /> LAG interface<br />
@@ -55,7 +71,9 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right">
{# Buttons #}
<td class="text-right text-nowrap">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit_termination or iface.connection %} {% if iface.circuit_termination or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
@@ -96,7 +114,7 @@
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface"> <a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
@@ -106,47 +124,53 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface"> <a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% with iface.ip_addresses.all as ipaddresses %} {% for ip in iface.ip_addresses.all %}
{% if ipaddresses %}
<tr class="ipaddress"> <tr class="ipaddress">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
{# Placeholder #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td> <td></td>
<td colspan="6" class="subtable">
{% else %}
<td colspan="7" class="subtable">
{% endif %} {% endif %}
<table class="table table-hover">
{% for ip in ipaddresses %} {# IP address #}
<tr> <td colspan="2">
<td>
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
{% endif %}
</td> </td>
{# Primary, status, role #}
<td> <td>
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %} {% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<span class="label label-success">Primary</span> <span class="label label-success">Primary</span>
{% endif %} {% endif %}
</td> <span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
<td> {% if ip.role %}
{% if ip.vrf %} <span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %}
<span class="text-muted">Global table</span>
{% endif %} {% endif %}
</td> </td>
<td>
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span> {# VRF #}
<td colspan="2">
{% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
{% else %}
<span class="text-muted">Global</span>
{% endif %}
</td> </td>
<td class="text-right">
{# Description #}
<td colspan="2">
{{ ip.description }}
</td>
{# Buttons #}
<td class="text-right text-nowrap">
{% if perms.ipam.change_ipaddress %} {% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs"> <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
@@ -160,8 +184,3 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table>
</td>
</tr>
{% endif %}
{% endwith %}

View File

@@ -1,5 +1,5 @@
<tr> <tr>
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td> <td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ item.manufacturer|default:"" }}</td> <td>{{ item.manufacturer|default:"" }}</td>
<td>{{ item.part_id }}</td> <td>{{ item.part_id }}</td>
@@ -11,7 +11,7 @@
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a> <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
{% endif %} {% endif %}
{% if perms.dcim.delete_inventoryitem %} {% if perms.dcim.delete_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a> <a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@@ -1,26 +1,25 @@
<tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}"> <tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ po.pk }}" /> <input name="pk" type="checkbox" value="{{ po.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ po.name }} <i class="fa fa-fw fa-bolt"></i> {{ po }}
</td> </td>
<td></td>
{% if po.connected_port %} {% if po.connected_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a> <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
</td> </td>
<td> <td>
{{ po.connected_port.name }} {{ po.connected_port }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_poweroutlet %} {% if perms.dcim.change_poweroutlet %}
{% if po.connected_port %} {% if po.connected_port %}
{% if po.connected_port.connection_status %} {% if po.connected_port.connection_status %}

View File

@@ -1,13 +1,13 @@
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}"> <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp }}
</td> </td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
</td> </td>
<td> <td>
{{ pp.power_outlet.name }} {{ pp.power_outlet }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">

View File

@@ -0,0 +1,5 @@
{% extends 'utilities/obj_bulk_delete.html' %}
{% block message_extra %}
<p class="text-center text-danger"><i class="fa fa-warning"></i> This will also delete all child inventory items of those listed.</p>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import inventory items
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='inventory items' %}
</div>
<h1>{% block title %}Inventory Items{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a platform Add a platform
</a> </a>
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import platforms
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Platforms{% endblock %}</h1> <h1>{% block title %}Platforms{% endblock %}</h1>

View File

@@ -24,32 +24,24 @@
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if prev_rack %} <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
<a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary"> <span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
<span class="fa fa-chevron-left" aria-hidden="true"></span>
Previous Rack
</a> </a>
{% endif %} <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
{% if next_rack %} <span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
<a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
<span class="fa fa-chevron-right" aria-hidden="true"></span>
Next Rack
</a> </a>
{% endif %}
{% if perms.dcim.change_rack %} {% if perms.dcim.change_rack %}
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning"> <a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack
Edit this rack
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_rack %} {% if perms.dcim.delete_rack %}
<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger"> <a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span> <span class="fa fa-trash" aria-hidden="true"></span> Delete this rack
Delete this rack
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1> <h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=rack %} {% include 'inc/created_updated.html' with obj=rack %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@@ -72,7 +64,7 @@
<td>Group</td> <td>Group</td>
<td> <td>
{% if rack.group %} {% if rack.group %}
<a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group.name }}</a> <a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@@ -93,7 +85,7 @@
<td> <td>
{% if rack.tenant %} {% if rack.tenant %}
{% if rack.tenant.group %} {% if rack.tenant.group %}
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a> <a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a> <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
@@ -173,7 +165,7 @@
{% for device in nonracked_devices %} {% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}> <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td> <td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a> <a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td> </td>
<td>{{ device.device_role }}</td> <td>{{ device.device_role }}</td>
<td>{{ device.device_type.full_name }}</td> <td>{{ device.device_type.full_name }}</td>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a rack role Add a rack role
</a> </a>
<a href="{% url 'dcim:rackrole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import rack roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Rack Roles{% endblock %}</h1> <h1>{% block title %}Rack Roles{% endblock %}</h1>

View File

@@ -17,8 +17,11 @@
</div> </div>
<h1>{% block title %}Regions{% endblock %}</h1> <h1>{% block title %}Regions{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
</div> </div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -76,7 +76,7 @@
<td> <td>
{% if site.tenant %} {% if site.tenant %}
{% if site.tenant.group %} {% if site.tenant.group %}
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a> <a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a> <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
@@ -211,6 +211,10 @@
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2> <h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
<p>Circuits</p> <p>Circuits</p>
</div> </div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site={{ site.slug }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
<p>Virtual Machines</p>
</div>
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
@@ -221,7 +225,7 @@
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
{% for rg in rack_groups %} {% for rg in rack_groups %}
<tr> <tr>
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td> <td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
<td>{{ rg.rack_count }}</td> <td>{{ rg.rack_count }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,18 @@
{% extends '500.html' %}
{% block message %}
<p>
A module import error occurred during this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
console and compare the output to the list of required packages.
</p>
<p>
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
running.
</p>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends '500.html' %}
{% block message %}
<p>
A file permission error was detected while processing this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Insufficient write permission to the media root</strong> - The configured
media root is <code>{{ settings.MEDIA_ROOT }}</code>. Ensure that the user NetBox runs as has access to write
files to all locations within this path.
</p>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends '500.html' %}
{% block message %}
<p>
A database programming error was detected while processing this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Database migrations missing</strong> - When upgrading to a new NetBox release, the upgrade script must
be run to apply any new database migrations. You can run migrations manually by executing
<code>python3 manage.py migrate</code> from the command line.
</p>
<p>
<i class="fa fa-warning"></i> <strong>Unsupported PostgreSQL version</strong> - Ensure that PostgreSQL version 9.4 or higher is in use. You
can check this by connecting to the database using NetBox's credentials and issuing a query for
<code>SELECT VERSION()</code>.
</p>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% if report.result.failed %} {% if result.failed %}
<label class="label label-danger">Failed</label> <label class="label label-danger">Failed</label>
{% elif report.result %} {% elif result %}
<label class="label label-success">Passed</label> <label class="label label-success">Passed</label>
{% else %} {% else %}
<label class="label label-default">N/A</label> <label class="label label-default">N/A</label>

View File

@@ -22,7 +22,7 @@
</form> </form>
</div> </div>
{% endif %} {% endif %}
<h1>{{ report.name }}{% include 'extras/inc/report_label.html' %}</h1> <h1>{{ report.name }}{% include 'extras/inc/report_label.html' with result=report.result %}</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% if report.description %} {% if report.description %}

View File

@@ -24,7 +24,7 @@
<a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a> <a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
</td> </td>
<td> <td>
{% include 'extras/inc/report_label.html' %} {% include 'extras/inc/report_label.html' with result=report.result %}
</td> </td>
<td>{{ report.description|default:"" }}</td> <td>{{ report.description|default:"" }}</td>
{% if report.result %} {% if report.result %}

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