mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-28 08:07:45 -06:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec0cb7a8bc | ||
|
|
841471104b | ||
|
|
ac71416eb9 | ||
|
|
779d685335 | ||
|
|
4d1e798c56 | ||
|
|
a598035236 | ||
|
|
50395aa821 | ||
|
|
6d9c8fd85b | ||
|
|
c3599bacf2 | ||
|
|
c10481b99d | ||
|
|
1cebc1248b | ||
|
|
c97f7041a7 | ||
|
|
89bfb4f722 | ||
|
|
da3935ff36 | ||
|
|
06810bff91 | ||
|
|
a9af75bbd1 | ||
|
|
be6ef15ffa | ||
|
|
e98f0c39d1 | ||
|
|
5666079d92 | ||
|
|
85f5ba9a25 | ||
|
|
df141a48d9 | ||
|
|
fed6fc131b | ||
|
|
cf49891853 | ||
|
|
de2a894269 | ||
|
|
34d10f8db7 | ||
|
|
68f76465cf | ||
|
|
45d6955260 | ||
|
|
30df060357 | ||
|
|
252be84bf0 | ||
|
|
40ab272995 | ||
|
|
0ec3b5db8b | ||
|
|
5dc9723585 | ||
|
|
50a451eddc | ||
|
|
3f8350b78f | ||
|
|
500a56b869 | ||
|
|
e50b7174bf | ||
|
|
8299c735b1 | ||
|
|
124878ed22 | ||
|
|
d888aa67f9 | ||
|
|
0cb3e1749b | ||
|
|
8ff10d5995 | ||
|
|
a5a7358d26 | ||
|
|
63ac8863f3 | ||
|
|
2047a16a57 | ||
|
|
8d6d55d628 | ||
|
|
9a7dd5ea19 | ||
|
|
30b544a743 | ||
|
|
a0bb7b08bd | ||
|
|
e1d655cb23 | ||
|
|
a1b1e261de | ||
|
|
9d50b78b69 | ||
|
|
00986fd7bf | ||
|
|
2519ebff9d | ||
|
|
c33775d71e | ||
|
|
6b0721cc21 | ||
|
|
d306e76420 | ||
|
|
73cd76932a | ||
|
|
5d19a9f50f | ||
|
|
368c30ef9d | ||
|
|
f77bf72de8 | ||
|
|
f2fbd92f78 | ||
|
|
480134302f | ||
|
|
74cc8c022c | ||
|
|
c6f3b00f0e | ||
|
|
626fbd1d10 | ||
|
|
b8df05cf88 | ||
|
|
e56fc4b1ee | ||
|
|
f9452163c5 | ||
|
|
76ebd2d34f | ||
|
|
85c273c8ca | ||
|
|
b9cd834e95 | ||
|
|
3067c3f262 | ||
|
|
cfa6bee081 | ||
|
|
b46cc2c1a9 | ||
|
|
5e734fc5a6 | ||
|
|
d08bc7767e | ||
|
|
bbd0761887 | ||
|
|
4668149943 | ||
|
|
a5b7c057eb | ||
|
|
0a04bb110a | ||
|
|
9c0b414676 | ||
|
|
5be4b0c4fd | ||
|
|
8e0eab20e2 | ||
|
|
1b5aa67f5d | ||
|
|
02c278f393 | ||
|
|
54472b3806 | ||
|
|
14e5f89feb | ||
|
|
a0b93bb4df | ||
|
|
85347d9675 | ||
|
|
060f7a7191 | ||
|
|
81ca6f7cba | ||
|
|
515645bb4d | ||
|
|
6ae6209457 | ||
|
|
272325ff05 | ||
|
|
a84b49b92d | ||
|
|
b63efdd80b | ||
|
|
7a64404299 | ||
|
|
2afa6ed2cb | ||
|
|
34f1a9ebfb | ||
|
|
6f2f8697ae | ||
|
|
6ec9d1d6ce | ||
|
|
047f22e110 | ||
|
|
a91fcbb310 | ||
|
|
5fc3eac0f6 | ||
|
|
937faaf149 | ||
|
|
115e7d6e50 | ||
|
|
34259d5d9d | ||
|
|
91b6ebb0c0 | ||
|
|
60b4f1f89f | ||
|
|
023ff6834a | ||
|
|
d00cab0b0a | ||
|
|
17493ff655 | ||
|
|
6c27e6c4fe | ||
|
|
4cb0be4df3 | ||
|
|
40f555a3b6 | ||
|
|
881fdbe893 | ||
|
|
a01c9ff379 |
25
.github/ISSUE_TEMPLATE.md
vendored
25
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,23 +4,30 @@
|
||||
remove the "is:open" filter from the search bar to include closed issues.
|
||||
|
||||
Check the appropriate type for your issue below by placing an x between the
|
||||
brackets. If none of the below apply, please raise your issue for
|
||||
discussion on our mailing list:
|
||||
brackets. For assistance with installation issues, or for any other issues
|
||||
other than those listed below, please raise your topic for discussion on
|
||||
our mailing list:
|
||||
|
||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
|
||||
Please note that issues which do not fall under any of the below categories
|
||||
will be closed.
|
||||
will be closed. Due to an excessive backlog of feature requests, we are
|
||||
not currently accepting any proposals which extend NetBox's feature scope.
|
||||
|
||||
Do not prepend any sort of tag to your issue's title. An administrator will
|
||||
review your issue and assign labels as appropriate.
|
||||
--->
|
||||
### Issue type
|
||||
[ ] Feature request <!-- Requesting the implementation of a new feature -->
|
||||
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
|
||||
[ ] Documentation <!-- Proposing a modification to the documentation -->
|
||||
[ ] Feature request <!-- An enhancement of existing functionality -->
|
||||
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
||||
[ ] Documentation <!-- A modification to the documentation -->
|
||||
|
||||
<!--
|
||||
Please describe the environment in which you are running NetBox. (Be sure
|
||||
to verify that you are running the latest stable release of NetBox before
|
||||
submitting a bug report.)
|
||||
submitting a bug report.) If you are submitting a bug report and have made
|
||||
any changes to the code base, please first validate that your bug can be
|
||||
recreated while running an official release.
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
@@ -28,8 +35,8 @@
|
||||
|
||||
<!--
|
||||
BUG REPORTS must include:
|
||||
* A list of the steps needed to reproduce the bug
|
||||
* A description of the expected behavior
|
||||
* A list of the steps needed for someone else to reproduce the bug
|
||||
* A description of the expected and observed behavior
|
||||
* Any relevant error messages (screenshots may also help)
|
||||
|
||||
FEATURE REQUESTS must include:
|
||||
|
||||
126
CONTRIBUTING.md
126
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
||||
## Getting Help
|
||||
|
||||
If you encounter any issues installing or using NetBox, try one of the
|
||||
following resources to get assistance. Please **do not** open a GitHub
|
||||
issue except to report bugs or request features.
|
||||
following resources to get assistance. Please **do not** open a GitHub issue
|
||||
except to report bugs or request features.
|
||||
|
||||
### Mailing List
|
||||
|
||||
@@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
|
||||
### Freenode IRC
|
||||
|
||||
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 use their [webchat client](https://webchat.freenode.net/).
|
||||
You can connect to Freenode at irc.freenode.net using an IRC client, or you can
|
||||
use their [webchat client](https://webchat.freenode.net/).
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
* 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
|
||||
has already been fixed.
|
||||
NetBox. If you're running an older version, it's possible that the bug has
|
||||
already been fixed.
|
||||
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
|
||||
already been reported. If you think you may be experiencing a reported
|
||||
issue that hasn't already been resolved, please click "add a reaction"
|
||||
in the top right corner of the issue and add a thumbs up (+1). You might
|
||||
also want to add a comment describing how it's affecting your
|
||||
installation. This will allow us to prioritize bugs based on how many
|
||||
users are affected.
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already
|
||||
been reported. If you think you may be experiencing a reported issue that
|
||||
hasn't already been resolved, please click "add a reaction" in the top right
|
||||
corner of the issue and add a thumbs up (+1). You mightalso want to add a
|
||||
comment describing how it's affecting your installation. This will allow us to
|
||||
prioritize bugs based on how many users are affected.
|
||||
|
||||
* If you haven't found an existing issue that describes your suspected
|
||||
bug, please inquire about it on the mailing list. **Do not** file an
|
||||
issue until you have received confirmation that it is in fact a bug.
|
||||
Invalid issues are very distracting and slow the pace at which NetBox is
|
||||
developed.
|
||||
* If you haven't found an existing issue that describes your suspected bug,
|
||||
please inquire about it on the mailing list. **Do not** file an issue until you
|
||||
have received confirmation that it is in fact a bug. Invalid issues are very
|
||||
distracting and slow the pace at which NetBox is developed.
|
||||
|
||||
* When submitting an issue, please be as descriptive as possible. Be
|
||||
sure to include:
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to
|
||||
include:
|
||||
|
||||
* The environment in which NetBox is running
|
||||
* The exact steps that can be taken to reproduce the issue (if
|
||||
applicable)
|
||||
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||
* Any error messages generated
|
||||
* Screenshots (if applicable)
|
||||
|
||||
@@ -49,71 +46,64 @@ sure to include:
|
||||
The issue will be reviewed by a moderator after submission and the appropriate
|
||||
labels will be applied.
|
||||
|
||||
* Keep in mind that we prioritize bugs based on their severity and how
|
||||
much work is required to resolve them. It may take some time for someone
|
||||
to address your issue.
|
||||
* Keep in mind that we prioritize bugs based on their severity and how much
|
||||
work is required to resolve them. It may take some time for someone to address
|
||||
your issue.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
||||
requesting is already listed. (Be sure to search closed issues as well,
|
||||
since some feature requests are rejected.) If the feature you'd like to
|
||||
see has already been requested, click "add a reaction" in the top right
|
||||
corner of the issue and add a thumbs up (+1). This ensures that the
|
||||
issue has a better chance of making it onto the roadmap. Also feel free
|
||||
to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your
|
||||
support.)
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
|
||||
is already listed. (Be sure to search closed issues as well, since some
|
||||
feature requests have been rejected.) If the feature you'd like to see has
|
||||
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 issue has
|
||||
a better chance of receiving attention. Also feel free to add a comment with
|
||||
any additional justification for the feature. (However, note that comments with
|
||||
no substance other than a "+1" will be deleted. Please use GitHub's reactions
|
||||
feature to indicate your support.)
|
||||
|
||||
* While suggestions for new features are welcome, it's important to
|
||||
limit the scope of NetBox's feature set to avoid feature creep. For
|
||||
example, the following features would be firmly out of scope for NetBox:
|
||||
|
||||
* Ticket management
|
||||
* Network state monitoring
|
||||
* Acting as a DNS server
|
||||
* Acting as an authentication server
|
||||
* Due to an excessive backlog of feature requests, we are not currently
|
||||
accepting any proposals which substantially extend NetBox's functionality
|
||||
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.
|
||||
|
||||
* Before filing a new feature request, consider raising your idea on the
|
||||
mailing list first. Feedback you receive there will help validate and
|
||||
shape the proposed feature before filing a formal issue.
|
||||
mailing list first. Feedback you receive there will help validate and shape the
|
||||
proposed feature before filing a formal issue.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to enumerate
|
||||
specific functionality and data schema. The more effort you put into
|
||||
writing a feature request, the better its chance is of being
|
||||
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||
describe the functionality and data model(s) being proposed. The more effort
|
||||
you put into writing a feature request, the better its chance is of being
|
||||
implemented. Overly broad feature requests will be closed.
|
||||
|
||||
* When submitting a feature request on GitHub, be sure to include the
|
||||
following:
|
||||
|
||||
* A detailed description of the proposed functionality
|
||||
* A use case for the feature; who would use it and what value it
|
||||
would add to NetBox
|
||||
* A rough description of changes necessary to the database schema
|
||||
(if applicable)
|
||||
* Any third-party libraries or other resources which would be
|
||||
involved
|
||||
* A use case for the feature; who would use it and what value it would add
|
||||
to NetBox
|
||||
* A rough description of changes necessary to the database schema (if
|
||||
applicable)
|
||||
* Any third-party libraries or other resources which would be involved
|
||||
|
||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
|
||||
The issue will be reviewed by a moderator after submission and the appropriate
|
||||
labels will be applied.
|
||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
|
||||
title. The issue will be reviewed by a moderator after submission and the
|
||||
appropriate labels will be applied.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
* Be sure to open an issue before starting work on a pull request, and
|
||||
discuss your idea with the NetBox maintainers before beginning work.
|
||||
This will help prevent wasting time on something that might we might not
|
||||
be able to implement. When suggesting a new feature, also make sure it
|
||||
won't conflict with any work that's already in progress.
|
||||
* Be sure to open an issue before starting work on a pull request, and discuss
|
||||
your idea with the NetBox maintainers before beginning work. This will help
|
||||
prevent wasting time on something that might we might not be able to implement.
|
||||
When suggesting a new feature, also make sure it won't conflict with any work
|
||||
that's already in progress.
|
||||
|
||||
* When submitting a pull request, please be sure to work off of the
|
||||
`develop` branch, rather than `master`. In NetBox, the `develop` branch
|
||||
is used for ongoing development, while `master` is used for tagging new
|
||||
stable releases.
|
||||
* When submitting a pull request, please be sure to work off of the `develop`
|
||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||
development, while `master` is used for tagging new stable releases.
|
||||
|
||||
* All code submissions should meet the following criteria (CI will
|
||||
enforce these checks):
|
||||
* All code submissions should meet the following criteria (CI will enforce
|
||||
these checks):
|
||||
|
||||
* Python syntax is valid
|
||||
* All tests pass when run with `./manage.py test`
|
||||
|
||||
@@ -31,6 +31,5 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
|
||||
|
||||
## Alternative Installations
|
||||
|
||||
* [Docker container](https://github.com/digitalocean/netbox-docker)
|
||||
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
|
||||
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||
|
||||
@@ -123,7 +123,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
|
||||
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)
|
||||
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
||||
> User-Agent: curl/7.35.0
|
||||
|
||||
@@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
|
||||
|
||||
# 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
|
||||
|
||||
@@ -52,10 +52,12 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
||||
LDAP_IGNORE_CERT_ERRORS = True
|
||||
```
|
||||
|
||||
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||
|
||||
## User Authentication
|
||||
|
||||
!!! info
|
||||
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
@@ -78,8 +80,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
||||
```
|
||||
|
||||
# User Groups for Permissions
|
||||
!!! Info
|
||||
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for AUTH_LDAP_GROUP_TYPE.
|
||||
!!! info
|
||||
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||
|
||||
@@ -94,6 +94,8 @@ The following methods are available to log results within a report:
|
||||
|
||||
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status.
|
||||
|
||||
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.
|
||||
|
||||
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
|
||||
|
||||
## Running Reports
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits import filters
|
||||
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.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from utilities.filters import NumericInFilter
|
||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
|
||||
@@ -78,12 +77,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
|
||||
@@ -11,7 +11,6 @@ from utilities.forms import (
|
||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||
SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||
from dcim.fields import ASNField
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from utilities.utils import csv_format
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
from .constants import *
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
{% 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>
|
||||
@@ -14,6 +14,21 @@ CIRCUITTYPE_ACTIONS = """
|
||||
"""
|
||||
|
||||
|
||||
class CircuitTerminationColumn(tables.Column):
|
||||
|
||||
def render(self, value):
|
||||
if value.interface:
|
||||
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
|
||||
value.interface.device.get_absolute_url(),
|
||||
value.site,
|
||||
value.interface.device
|
||||
))
|
||||
return mark_safe('<a href="{}">{}</a>'.format(
|
||||
value.site.get_absolute_url(),
|
||||
value.site
|
||||
))
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
@@ -61,15 +76,9 @@ class CircuitTable(BaseTable):
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
a_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||
args=[Accessor('termination_a.site.slug')]
|
||||
)
|
||||
z_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||
args=[Accessor('termination_z.site.slug')]
|
||||
)
|
||||
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
||||
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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.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 extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||
from extras.models import Graph
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
@@ -134,7 +135,11 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class CircuitListView(ObjectListView):
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
queryset = Circuit.objects.select_related(
|
||||
'provider', 'type', 'tenant'
|
||||
).prefetch_related(
|
||||
'terminations__site', 'terminations__interface__device'
|
||||
)
|
||||
filter = filters.CircuitFilter
|
||||
filter_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from ipam.models import IPAddress
|
||||
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 (
|
||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
||||
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
from virtualization.models import Cluster
|
||||
@@ -221,7 +225,7 @@ class WritableRackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'rack', 'units', 'description']
|
||||
fields = ['id', 'rack', 'units', 'user', 'description']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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.mixins import ListModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
from dcim import filters
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin
|
||||
from .exceptions import MissingFilterException
|
||||
from utilities.api import (
|
||||
IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
|
||||
)
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
|
||||
#
|
||||
@@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
device.platform
|
||||
))
|
||||
|
||||
# Check that NAPALM is installed and verify the configured driver
|
||||
# Check that NAPALM is installed
|
||||
try:
|
||||
import napalm
|
||||
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
||||
except ImportError:
|
||||
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:
|
||||
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||
except ModuleImportError:
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
||||
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 (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
class RegionFilter(django_filters.FilterSet):
|
||||
parent_id = NullableModelMultipleChoiceFilter(
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
)
|
||||
parent = NullableModelMultipleChoiceFilter(
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
name='parent__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Parent region (slug)',
|
||||
@@ -42,20 +44,22 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = NullableModelMultipleChoiceFilter(
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = NullableModelMultipleChoiceFilter(
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='region__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -126,32 +130,32 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role__slug',
|
||||
queryset=RackRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
@@ -159,7 +163,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@@ -193,13 +197,13 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
@@ -326,7 +330,7 @@ class DeviceRoleFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['name', 'slug', 'color']
|
||||
fields = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilter(django_filters.FilterSet):
|
||||
@@ -368,22 +372,22 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
platform_id = NullableModelMultipleChoiceFilter(
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = NullableModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
@@ -405,12 +409,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
rack_id = NullableModelMultipleChoiceFilter(
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
cluster_id = NullableModelMultipleChoiceFilter(
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cluster.objects.all(),
|
||||
label='VM cluster (ID)',
|
||||
)
|
||||
@@ -421,7 +425,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Device model (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=STATUS_CHOICES
|
||||
choices=STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
is_full_depth = django_filters.BooleanFilter(
|
||||
name='device_type__is_full_depth',
|
||||
@@ -450,7 +455,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['serial']
|
||||
fields = ['serial', 'position']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -595,7 +600,7 @@ class DeviceBayFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
parent_id = NullableModelMultipleChoiceFilter(
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import EUI, AddrFormatError
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from netaddr import EUI, AddrFormatError
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mptt.forms import TreeNodeChoiceField
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.db.models import Count, Q
|
||||
from mptt.forms import TreeNodeChoiceField
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress
|
||||
@@ -17,17 +18,20 @@ from utilities.forms import (
|
||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||
SlugField, FilterTreeNodeMultipleChoiceField,
|
||||
)
|
||||
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 .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
||||
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
|
||||
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
|
||||
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,
|
||||
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||
RackRole, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
|
||||
|
||||
@@ -375,10 +379,11 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
|
||||
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
|
||||
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['units', 'description']
|
||||
fields = ['units', 'user', 'description']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -410,6 +415,15 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -900,11 +914,20 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
required=False,
|
||||
help_text='Mounted rack face'
|
||||
)
|
||||
cluster = forms.ModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtualization cluster',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid cluster name.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -940,11 +963,20 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
device_bay_name = forms.CharField(
|
||||
help_text='Name of device bay',
|
||||
)
|
||||
cluster = forms.ModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtualization cluster',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid cluster name.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay_name',
|
||||
'parent', 'device_bay_name', 'cluster',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
22
netbox/dcim/migrations/0049_rackreservation_change_user.py
Normal file
22
netbox/dcim/migrations/0049_rackreservation_change_user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-31 17:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0048_rack_serial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -1,12 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from itertools import count, groupby
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
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.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
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 tenancy.models import Tenant
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
@@ -144,6 +143,11 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
def count_circuits(self):
|
||||
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
|
||||
@@ -256,8 +260,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
if self.pk:
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
@@ -267,6 +271,12 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
min_height
|
||||
)
|
||||
})
|
||||
# Validate that Rack was assigned a group of its same site, if applicable
|
||||
if self.group:
|
||||
if self.group.site != self.site:
|
||||
raise ValidationError({
|
||||
'group': "Rack group must be from the same site, {}.".format(self.site)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -290,6 +300,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.role.name if self.role else None,
|
||||
self.get_type_display() if self.type else None,
|
||||
self.serial,
|
||||
self.width,
|
||||
self.u_height,
|
||||
self.desc_units,
|
||||
@@ -411,7 +422,7 @@ class RackReservation(models.Model):
|
||||
rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
|
||||
units = ArrayField(models.PositiveSmallIntegerField())
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
|
||||
user = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
description = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
@@ -1083,16 +1094,11 @@ class ConsolePort(models.Model):
|
||||
class ConsoleServerPortManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
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 ...
|
||||
"""
|
||||
# Pad any trailing digits to effect natural sorting
|
||||
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
||||
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)",
|
||||
}).order_by('device', 'name_as_integer')
|
||||
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
|
||||
"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -1111,6 +1117,17 @@ class ConsoleServerPort(models.Model):
|
||||
def __str__(self):
|
||||
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
|
||||
@@ -1154,9 +1171,10 @@ class PowerPort(models.Model):
|
||||
class PowerOutletManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
# Pad any trailing digits to effect natural sorting
|
||||
return super(PowerOutletManager, self).get_queryset().extra(select={
|
||||
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), "
|
||||
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))",
|
||||
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
|
||||
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
@@ -1176,6 +1194,17 @@ class PowerOutlet(models.Model):
|
||||
def __str__(self):
|
||||
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
|
||||
@@ -1232,6 +1261,14 @@ class Interface(models.Model):
|
||||
|
||||
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
|
||||
if self.device and self.virtual_machine:
|
||||
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
|
||||
|
||||
@@ -44,10 +44,10 @@ class InterfaceQuerySet(QuerySet):
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from .models import (
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
REGION_LINK = """
|
||||
{% if record.get_children %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
||||
@@ -154,11 +153,12 @@ class SiteDetailTable(SiteTable):
|
||||
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')
|
||||
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):
|
||||
fields = (
|
||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count',
|
||||
'vlan_count', 'circuit_count', 'vm_count',
|
||||
)
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@ class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
vm_count = tables.Column(verbose_name='VMs')
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
@@ -369,7 +370,7 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'color', 'vm_role', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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.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 (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
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')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
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(
|
||||
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')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
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(
|
||||
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')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
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(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
|
||||
@@ -9,14 +9,29 @@ class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.site = Site.objects.create(
|
||||
self.site1 = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
slug='test-site-1'
|
||||
)
|
||||
self.site2 = Site.objects.create(
|
||||
name='TestSite2',
|
||||
slug='test-site-2'
|
||||
)
|
||||
self.group1 = RackGroup.objects.create(
|
||||
name='TestGroup1',
|
||||
slug='test-group-1',
|
||||
site=self.site1
|
||||
)
|
||||
self.group2 = RackGroup.objects.create(
|
||||
name='TestGroup2',
|
||||
slug='test-group-2',
|
||||
site=self.site2
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site,
|
||||
site=self.site1,
|
||||
group=self.group1,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
@@ -57,13 +72,51 @@ class RackTestCase(TestCase):
|
||||
|
||||
}
|
||||
|
||||
def test_rack_device_outside_height(self):
|
||||
|
||||
rack1 = Rack(
|
||||
name='TestRack2',
|
||||
facility_id='A102',
|
||||
site=self.site1,
|
||||
u_height=42
|
||||
)
|
||||
rack1.save()
|
||||
|
||||
device1 = Device(
|
||||
name='TestSwitch1',
|
||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||
device_role=DeviceRole.objects.get(slug='switch'),
|
||||
site=self.site1,
|
||||
rack=rack1,
|
||||
position=43,
|
||||
face=RACK_FACE_FRONT,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
rack1.clean()
|
||||
|
||||
def test_rack_group_site(self):
|
||||
|
||||
rack_invalid_group = Rack(
|
||||
name='TestRack2',
|
||||
facility_id='A102',
|
||||
site=self.site1,
|
||||
u_height=42,
|
||||
group=self.group2
|
||||
)
|
||||
rack_invalid_group.save()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
rack_invalid_group.clean()
|
||||
|
||||
def test_mount_single_device(self):
|
||||
|
||||
device1 = Device(
|
||||
name='TestSwitch1',
|
||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||
device_role=DeviceRole.objects.get(slug='switch'),
|
||||
site=self.site,
|
||||
site=self.site1,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
@@ -92,7 +145,7 @@ class RackTestCase(TestCase):
|
||||
name='TestPDU',
|
||||
device_role=self.role.get('PDU'),
|
||||
device_type=self.device_type.get('cc5000'),
|
||||
site=self.site,
|
||||
site=self.site1,
|
||||
rack=self.rack,
|
||||
position=None,
|
||||
face=None,
|
||||
|
||||
@@ -5,9 +5,8 @@ from django.conf.urls import url
|
||||
from extras.views import ImageAttachmentEditView
|
||||
from ipam.views import ServiceCreateView
|
||||
from secrets.views import secret_add
|
||||
from .models import Device, Rack, Site
|
||||
from . import views
|
||||
|
||||
from .models import Device, Rack, Site
|
||||
|
||||
app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
@@ -45,6 +44,7 @@ urlpatterns = [
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
from natsort import natsorted
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
from django.contrib import messages
|
||||
@@ -15,51 +14,28 @@ from django.utils.html import escape
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from natsort import natsorted
|
||||
|
||||
from ipam.models import Prefix, Service, VLAN
|
||||
from circuits.models import Circuit
|
||||
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.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
|
||||
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .constants import CONNECTION_STATUS_CONNECTED
|
||||
from .models import (
|
||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, Region, Site,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
EXPANSION_PATTERN = '\[(\d+-\d+)\]'
|
||||
|
||||
|
||||
def xstr(s):
|
||||
"""
|
||||
Replace None with an empty string (for CSV export)
|
||||
"""
|
||||
return '' if s is None else str(s)
|
||||
|
||||
|
||||
def expand_pattern(string):
|
||||
"""
|
||||
Expand a numeric pattern into a list of strings. Examples:
|
||||
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
|
||||
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
|
||||
"""
|
||||
lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x), int(y) + 1):
|
||||
if remnant:
|
||||
for string in expand_pattern(remnant):
|
||||
yield "{0}{1}{2}".format(lead, i, string)
|
||||
else:
|
||||
yield "{0}{1}".format(lead, i)
|
||||
|
||||
|
||||
class BulkDisconnectView(View):
|
||||
"""
|
||||
An extendable view for disconnection console/power/interface components in bulk.
|
||||
@@ -159,6 +135,7 @@ class SiteView(View):
|
||||
'prefix_count': Prefix.objects.filter(site=site).count(),
|
||||
'vlan_count': VLAN.objects.filter(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'))
|
||||
topology_maps = TopologyMap.objects.filter(site=site)
|
||||
@@ -452,6 +429,16 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
cls = RackReservation
|
||||
queryset = RackReservation.objects.select_related('rack', 'user')
|
||||
filter = filters.RackReservationFilter
|
||||
table = tables.RackReservationTable
|
||||
form = forms.RackReservationBulkEditForm
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
@@ -543,12 +530,12 @@ class DeviceTypeView(View):
|
||||
show_header=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_devicetype'):
|
||||
consoleport_table.base_columns['pk'].visible = True
|
||||
consoleserverport_table.base_columns['pk'].visible = True
|
||||
powerport_table.base_columns['pk'].visible = True
|
||||
poweroutlet_table.base_columns['pk'].visible = True
|
||||
interface_table.base_columns['pk'].visible = True
|
||||
devicebay_table.base_columns['pk'].visible = True
|
||||
consoleport_table.columns.show('pk')
|
||||
consoleserverport_table.columns.show('pk')
|
||||
powerport_table.columns.show('pk')
|
||||
poweroutlet_table.columns.show('pk')
|
||||
interface_table.columns.show('pk')
|
||||
devicebay_table.columns.show('pk')
|
||||
|
||||
return render(request, 'dcim/devicetype.html', {
|
||||
'devicetype': devicetype,
|
||||
@@ -726,7 +713,10 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceRoleListView(ObjectListView):
|
||||
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
device_count=Count('devices', distinct=True),
|
||||
vm_count=Count('virtual_machines', distinct=True)
|
||||
)
|
||||
table = tables.DeviceRoleTable
|
||||
template_name = 'dcim/devicerole_list.html'
|
||||
|
||||
@@ -820,15 +810,11 @@ class DeviceView(View):
|
||||
console_ports = natsorted(
|
||||
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||
)
|
||||
cs_ports = natsorted(
|
||||
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
|
||||
)
|
||||
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||
power_ports = natsorted(
|
||||
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
||||
)
|
||||
power_outlets = natsorted(
|
||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||
)
|
||||
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||
interfaces = Interface.objects.order_naturally(
|
||||
device.device_type.interface_ordering
|
||||
).filter(
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
from datetime import datetime
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
||||
)
|
||||
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
|
||||
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
# Data validation
|
||||
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
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError(
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||
from dcim.models import Device, Rack, Site
|
||||
from extras.models import (
|
||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction,
|
||||
)
|
||||
from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
|
||||
from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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.http import Http404, HttpResponse
|
||||
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.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
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):
|
||||
@@ -19,17 +19,28 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
|
||||
# Skip filter on empty value
|
||||
if not value.strip():
|
||||
return queryset
|
||||
# Treat 0 as None for Select fields
|
||||
try:
|
||||
if self.cf_type == CF_TYPE_SELECT and int(value) == 0:
|
||||
return queryset.exclude(
|
||||
custom_field_values__field__name=self.name,
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Selection fields get special treatment (values must be integers)
|
||||
if self.cf_type == CF_TYPE_SELECT:
|
||||
try:
|
||||
# Treat 0 as None
|
||||
if int(value) == 0:
|
||||
return queryset.exclude(
|
||||
custom_field_values__field__name=self.name,
|
||||
)
|
||||
# Match on exact CustomFieldChoice PK
|
||||
else:
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value=value,
|
||||
)
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value__icontains=value,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
||||
from .models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
|
||||
ImageAttachment,
|
||||
)
|
||||
from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||
|
||||
|
||||
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from getpass import getpass
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
from paramiko import AuthenticationException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
from paramiko import AuthenticationException
|
||||
|
||||
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from extras.models import ReportResult
|
||||
from extras.reports import get_reports
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
||||
from __future__ import unicode_literals
|
||||
from distutils.version import StrictVersion
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
@@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
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'):
|
||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
import graphviz
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -274,6 +275,7 @@ class TopologyMap(models.Model):
|
||||
# Construct the graph
|
||||
graph = graphviz.Graph()
|
||||
graph.graph_attr['ranksep'] = '1'
|
||||
seen = set()
|
||||
for i, device_set in enumerate(self.device_sets):
|
||||
|
||||
subgraph = graphviz.Graph(name='sg{}'.format(i))
|
||||
@@ -288,6 +290,9 @@ class TopologyMap(models.Model):
|
||||
devices = []
|
||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
||||
# Remove duplicate devices
|
||||
devices = [d for d in devices if d.id not in seen]
|
||||
seen.update([d.id for d in devices])
|
||||
for d in devices:
|
||||
bg_color = '#{}'.format(d.device_role.color)
|
||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -177,3 +178,12 @@ class Report(object):
|
||||
result = ReportResult(report=self.full_name, failed=self.failed, data=self._results)
|
||||
result.save()
|
||||
self.result = result
|
||||
|
||||
# Perform any post-run tasks
|
||||
self.post_run()
|
||||
|
||||
def post_run(self):
|
||||
"""
|
||||
Extend this method to include any tasks which should execute after the report has been run.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from ncclient import manager
|
||||
import paramiko
|
||||
import xmltodict
|
||||
|
||||
from ncclient import manager
|
||||
|
||||
CONNECT_TIMEOUT = 5 # seconds
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
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 utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
from datetime import date
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.models import (
|
||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
||||
CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
)
|
||||
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
||||
|
||||
from extras import views
|
||||
|
||||
|
||||
app_name = 'extras'
|
||||
urlpatterns = [
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
@@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import (
|
||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
|
||||
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
from ipam.constants import (
|
||||
IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
|
||||
)
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||
@@ -240,12 +241,22 @@ class WritablePrefixSerializer(CustomFieldModelSerializer):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressInterfaceSerializer(InterfaceSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'device', 'virtual_machine', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address',
|
||||
'mgmt_only', 'description', 'is_connected', 'interface_connection', 'circuit_termination',
|
||||
]
|
||||
|
||||
|
||||
class IPAddressSerializer(CustomFieldModelSerializer):
|
||||
vrf = NestedVRFSerializer()
|
||||
tenant = NestedTenantSerializer()
|
||||
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
||||
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
|
||||
interface = InterfaceSerializer()
|
||||
interface = IPAddressInterfaceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -262,6 +273,7 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
|
||||
model = IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
|
||||
|
||||
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
|
||||
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
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.decorators import detail_route
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
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 ipam import filters
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||
from . import serializers
|
||||
|
||||
@@ -151,7 +150,7 @@ class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
queryset = IPAddress.objects.select_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside'
|
||||
).prefetch_related(
|
||||
'interface__device'
|
||||
'interface__device', 'interface__virtual_machine'
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
write_serializer_class = serializers.WritableIPAddressSerializer
|
||||
|
||||
@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
|
||||
(VLAN_STATUS_DEPRECATED, 'Deprecated')
|
||||
)
|
||||
|
||||
# Bootstrap CSS classes for various statuses
|
||||
# Bootstrap CSS classes
|
||||
STATUS_CHOICE_CLASSES = {
|
||||
0: 'default',
|
||||
1: 'primary',
|
||||
@@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
|
||||
4: 'warning',
|
||||
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_PROTOCOL_TCP = 6
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from netaddr import IPNetwork
|
||||
|
||||
from .formfields import IPFormField
|
||||
from .lookups import (
|
||||
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
|
||||
NetHost, NetHostContained, NetMaskLength, Regex, StartsWith,
|
||||
)
|
||||
from . import lookups
|
||||
|
||||
|
||||
def prefix_validator(prefix):
|
||||
@@ -58,17 +54,18 @@ class IPNetworkField(BaseIPField):
|
||||
return 'cidr'
|
||||
|
||||
|
||||
IPNetworkField.register_lookup(EndsWith)
|
||||
IPNetworkField.register_lookup(IEndsWith)
|
||||
IPNetworkField.register_lookup(StartsWith)
|
||||
IPNetworkField.register_lookup(IStartsWith)
|
||||
IPNetworkField.register_lookup(Regex)
|
||||
IPNetworkField.register_lookup(IRegex)
|
||||
IPNetworkField.register_lookup(NetContained)
|
||||
IPNetworkField.register_lookup(NetContainedOrEqual)
|
||||
IPNetworkField.register_lookup(NetContains)
|
||||
IPNetworkField.register_lookup(NetContainsOrEquals)
|
||||
IPNetworkField.register_lookup(NetMaskLength)
|
||||
IPNetworkField.register_lookup(lookups.IExact)
|
||||
IPNetworkField.register_lookup(lookups.EndsWith)
|
||||
IPNetworkField.register_lookup(lookups.IEndsWith)
|
||||
IPNetworkField.register_lookup(lookups.StartsWith)
|
||||
IPNetworkField.register_lookup(lookups.IStartsWith)
|
||||
IPNetworkField.register_lookup(lookups.Regex)
|
||||
IPNetworkField.register_lookup(lookups.IRegex)
|
||||
IPNetworkField.register_lookup(lookups.NetContained)
|
||||
IPNetworkField.register_lookup(lookups.NetContainedOrEqual)
|
||||
IPNetworkField.register_lookup(lookups.NetContains)
|
||||
IPNetworkField.register_lookup(lookups.NetContainsOrEquals)
|
||||
IPNetworkField.register_lookup(lookups.NetMaskLength)
|
||||
|
||||
|
||||
class IPAddressField(BaseIPField):
|
||||
@@ -81,16 +78,17 @@ class IPAddressField(BaseIPField):
|
||||
return 'inet'
|
||||
|
||||
|
||||
IPAddressField.register_lookup(EndsWith)
|
||||
IPAddressField.register_lookup(IEndsWith)
|
||||
IPAddressField.register_lookup(StartsWith)
|
||||
IPAddressField.register_lookup(IStartsWith)
|
||||
IPAddressField.register_lookup(Regex)
|
||||
IPAddressField.register_lookup(IRegex)
|
||||
IPAddressField.register_lookup(NetContained)
|
||||
IPAddressField.register_lookup(NetContainedOrEqual)
|
||||
IPAddressField.register_lookup(NetContains)
|
||||
IPAddressField.register_lookup(NetContainsOrEquals)
|
||||
IPAddressField.register_lookup(NetHost)
|
||||
IPAddressField.register_lookup(NetHostContained)
|
||||
IPAddressField.register_lookup(NetMaskLength)
|
||||
IPAddressField.register_lookup(lookups.IExact)
|
||||
IPAddressField.register_lookup(lookups.EndsWith)
|
||||
IPAddressField.register_lookup(lookups.IEndsWith)
|
||||
IPAddressField.register_lookup(lookups.StartsWith)
|
||||
IPAddressField.register_lookup(lookups.IStartsWith)
|
||||
IPAddressField.register_lookup(lookups.Regex)
|
||||
IPAddressField.register_lookup(lookups.IRegex)
|
||||
IPAddressField.register_lookup(lookups.NetContained)
|
||||
IPAddressField.register_lookup(lookups.NetContainedOrEqual)
|
||||
IPAddressField.register_lookup(lookups.NetContains)
|
||||
IPAddressField.register_lookup(lookups.NetContainsOrEquals)
|
||||
IPAddressField.register_lookup(lookups.NetHost)
|
||||
IPAddressField.register_lookup(lookups.NetHostContained)
|
||||
IPAddressField.register_lookup(lookups.NetMaskLength)
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from netaddr import IPNetwork
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django.db.models import Q
|
||||
import netaddr
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from utilities.filters import NumericInFilter
|
||||
from virtualization.models import VirtualMachine
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
||||
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
)
|
||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@@ -23,12 +20,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -82,7 +79,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
@@ -102,45 +99,58 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
# TODO: Deprecate in v2.3.0
|
||||
parent = django_filters.CharFilter(
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
method='search_within_include',
|
||||
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(
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
)
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
label='VRF',
|
||||
)
|
||||
vrf = NullableModelMultipleChoiceFilter(
|
||||
name='vrf',
|
||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||
name='vrf__rd',
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
vlan_id = NullableModelMultipleChoiceFilter(
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLAN.objects.all(),
|
||||
label='VLAN (ID)',
|
||||
)
|
||||
@@ -148,18 +158,19 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
name='vlan__vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PREFIX_STATUS_CHOICES
|
||||
choices=PREFIX_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -171,22 +182,46 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
def search_within(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
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)
|
||||
except (AddrFormatError, ValueError):
|
||||
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):
|
||||
if not value:
|
||||
return queryset
|
||||
@@ -207,22 +242,22 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
)
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
label='VRF',
|
||||
)
|
||||
vrf = NullableModelMultipleChoiceFilter(
|
||||
name='vrf',
|
||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||
name='vrf__rd',
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -254,7 +289,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Interface (ID)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPADDRESS_STATUS_CHOICES
|
||||
choices=IPADDRESS_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=IPADDRESS_ROLE_CHOICES
|
||||
@@ -267,12 +303,10 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
ipaddress = str(IPNetwork(value.strip()))
|
||||
qs_filter |= Q(address__net_host=ipaddress)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
qs_filter = (
|
||||
Q(description__icontains=value) |
|
||||
Q(address__istartswith=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
@@ -280,7 +314,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
query = str(IPNetwork(value.strip()).cidr)
|
||||
query = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||
return queryset.filter(address__net_host_contained=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
@@ -292,12 +326,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class VLANGroupFilter(django_filters.FilterSet):
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@@ -314,48 +348,49 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group__slug',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VLAN_STATUS_CHOICES
|
||||
choices=VLAN_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork, AddrFormatError
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from netaddr import IPNetwork, AddrFormatError
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -14,11 +14,8 @@ from utilities.forms import (
|
||||
add_blank_choice,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
||||
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
)
|
||||
|
||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
IP_FAMILY_CHOICES = [
|
||||
('', 'All'),
|
||||
@@ -362,7 +359,7 @@ def prefix_status_choices():
|
||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Prefix
|
||||
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',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
|
||||
@@ -691,6 +688,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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():
|
||||
status_counts = {}
|
||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Lookup, Transform, IntegerField
|
||||
from django.db.models.lookups import BuiltinLookup
|
||||
from django.db.models import lookups
|
||||
|
||||
|
||||
class NetFieldDecoratorMixin(object):
|
||||
@@ -13,28 +13,40 @@ class NetFieldDecoratorMixin(object):
|
||||
return lhs_string, lhs_params
|
||||
|
||||
|
||||
class EndsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'endswith'
|
||||
class IExact(NetFieldDecoratorMixin, lookups.IExact):
|
||||
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
return '= LOWER(%s)' % rhs
|
||||
|
||||
|
||||
class IEndsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'iendswith'
|
||||
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
|
||||
pass
|
||||
|
||||
|
||||
class StartsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
|
||||
pass
|
||||
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
return 'LIKE LOWER(%s)' % rhs
|
||||
|
||||
|
||||
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
|
||||
lookup_name = 'startswith'
|
||||
|
||||
|
||||
class IStartsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'istartswith'
|
||||
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
|
||||
pass
|
||||
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
return 'LIKE LOWER(%s)' % rhs
|
||||
|
||||
|
||||
class Regex(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'regex'
|
||||
class Regex(NetFieldDecoratorMixin, lookups.Regex):
|
||||
pass
|
||||
|
||||
|
||||
class IRegex(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'iregex'
|
||||
class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
|
||||
pass
|
||||
|
||||
|
||||
class NetContainsOrEquals(Lookup):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
import netaddr
|
||||
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@@ -282,11 +281,27 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
def get_duplicates(self):
|
||||
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.
|
||||
"""
|
||||
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||
|
||||
def get_child_ips(self):
|
||||
"""
|
||||
Return all IPAddresses within this Prefix.
|
||||
Return all IPAddresses within this Prefix and VRF.
|
||||
"""
|
||||
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
|
||||
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):
|
||||
"""
|
||||
@@ -305,6 +320,24 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
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):
|
||||
"""
|
||||
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
||||
@@ -315,25 +348,12 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
else:
|
||||
child_count = IPAddress.objects.filter(
|
||||
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
|
||||
).count()
|
||||
child_count = self.get_child_ips().count()
|
||||
prefix_size = self.prefix.size
|
||||
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
|
||||
prefix_size -= 2
|
||||
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):
|
||||
|
||||
@@ -440,7 +460,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.get_status_display(),
|
||||
self.get_role_display(),
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.interface.name if self.interface else None,
|
||||
is_primary,
|
||||
self.description,
|
||||
@@ -452,9 +472,18 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
@property
|
||||
def virtual_machine(self):
|
||||
if self.interface:
|
||||
return self.interface.virtual_machine
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
def get_role_class(self):
|
||||
return ROLE_CHOICE_CLASSES[self.role]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VLANGroup(models.Model):
|
||||
|
||||
@@ -6,7 +6,6 @@ from django_tables2.utils import Accessor
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
RIR_UTILIZATION = """
|
||||
<div class="progress">
|
||||
{% if record.stats.total %}
|
||||
@@ -77,6 +76,10 @@ IPADDRESS_LINK = """
|
||||
{% 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 = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
||||
@@ -269,8 +272,8 @@ class PrefixDetailTable(PrefixTable):
|
||||
class IPAddressTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
@@ -294,6 +297,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
|
||||
#
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
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 ipam.models import (
|
||||
Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF,
|
||||
)
|
||||
from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import netaddr
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
||||
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+)/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'),
|
||||
|
||||
# IP addresses
|
||||
@@ -60,6 +61,7 @@ urlpatterns = [
|
||||
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/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+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_tables2 import RequestConfig
|
||||
import netaddr
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
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.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
@@ -17,11 +16,8 @@ from utilities.views import (
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .constants import IPADDRESS_ROLE_ANYCAST
|
||||
from .models import (
|
||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
||||
Service, VLAN, VLANGroup, VRF,
|
||||
)
|
||||
from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
@@ -325,7 +321,7 @@ class AggregateView(View):
|
||||
|
||||
prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
prefix_table.columns.show('pk')
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
@@ -458,11 +454,6 @@ class PrefixView(View):
|
||||
except Aggregate.DoesNotExist:
|
||||
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 = Prefix.objects.filter(
|
||||
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
|
||||
@@ -485,6 +476,20 @@ class PrefixView(View):
|
||||
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
|
||||
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 = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||
@@ -493,15 +498,16 @@ class PrefixView(View):
|
||||
).annotate_depth(limit=0)
|
||||
if 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'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
prefix_table.columns.show('pk')
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'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
|
||||
permissions = {
|
||||
@@ -510,15 +516,12 @@ class PrefixView(View):
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix.html', {
|
||||
return render(request, 'ipam/prefix_prefixes.html', {
|
||||
'prefix': prefix,
|
||||
'aggregate': aggregate,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'child_prefix_table': child_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
'first_available_prefix': prefix.get_first_available_prefix(),
|
||||
'prefix_table': prefix_table,
|
||||
'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,16 +532,14 @@ class PrefixIPAddressesView(View):
|
||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = IPAddress.objects.filter(
|
||||
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
ipaddresses = prefix.get_child_ips().select_related(
|
||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||
)
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
ip_table.columns.show('pk')
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
@@ -555,9 +556,10 @@ class PrefixIPAddressesView(View):
|
||||
|
||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||
'prefix': prefix,
|
||||
'first_available_ip': prefix.get_first_available_ip(),
|
||||
'ip_table': ip_table,
|
||||
'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'
|
||||
|
||||
|
||||
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):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
model = IPAddress
|
||||
|
||||
@@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
|
||||
def show_form_for_method(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
def get_filter_form(self, data, view, request):
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Authentication
|
||||
|
||||
@@ -4,7 +4,6 @@ from django import forms
|
||||
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
|
||||
OBJ_TYPE_CHOICES = (
|
||||
('', 'All Objects'),
|
||||
('Circuits', (
|
||||
@@ -30,12 +29,16 @@ OBJ_TYPE_CHOICES = (
|
||||
('Tenancy', (
|
||||
('tenant', 'Tenants'),
|
||||
)),
|
||||
('Virtualization', (
|
||||
('cluster', 'Clusters'),
|
||||
('virtualmachine', 'Virtual machines'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
class SearchForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'})
|
||||
label='Search'
|
||||
)
|
||||
obj_type = forms.ChoiceField(
|
||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.2.1'
|
||||
VERSION = '2.2.8'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -148,6 +148,7 @@ MIDDLEWARE = (
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'utilities.middleware.ExceptionHandlingMiddleware',
|
||||
'utilities.middleware.LoginRequiredMiddleware',
|
||||
'utilities.middleware.APIVersionMiddleware',
|
||||
)
|
||||
@@ -206,6 +207,10 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
|
||||
# Django filters
|
||||
FILTERS_NULL_CHOICE_LABEL = 'None'
|
||||
FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string
|
||||
|
||||
# Django REST framework (API)
|
||||
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
|
||||
REST_FRAMEWORK = {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework_swagger.views import get_swagger_view
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
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
|
||||
|
||||
|
||||
handler500 = handle_500
|
||||
swagger_view = get_swagger_view(title='NetBox API')
|
||||
|
||||
_patterns = [
|
||||
@@ -48,9 +45,6 @@ _patterns = [
|
||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
|
||||
# Error testing
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
# Admin
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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.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.models import Circuit, Provider
|
||||
@@ -27,10 +27,9 @@ from tenancy.models import Tenant
|
||||
from tenancy.tables import TenantTable
|
||||
from virtualization.filters import ClusterFilter, VirtualMachineFilter
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from virtualization.tables import ClusterTable, VirtualMachineTable
|
||||
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
||||
from .forms import SearchForm
|
||||
|
||||
|
||||
SEARCH_MAX_RESULTS = 15
|
||||
SEARCH_TYPES = OrderedDict((
|
||||
# Circuits
|
||||
@@ -60,7 +59,7 @@ SEARCH_TYPES = OrderedDict((
|
||||
'url': 'dcim:rack_list',
|
||||
}),
|
||||
('devicetype', {
|
||||
'queryset': DeviceType.objects.select_related('manufacturer'),
|
||||
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
||||
'filter': DeviceTypeFilter,
|
||||
'table': DeviceTypeTable,
|
||||
'url': 'dcim:devicetype_list',
|
||||
@@ -126,9 +125,11 @@ SEARCH_TYPES = OrderedDict((
|
||||
'url': 'virtualization:cluster_list',
|
||||
}),
|
||||
('virtualmachine', {
|
||||
'queryset': VirtualMachine.objects.select_related('cluster', 'tenant', 'platform'),
|
||||
'queryset': VirtualMachine.objects.select_related(
|
||||
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
),
|
||||
'filter': VirtualMachineFilter,
|
||||
'table': VirtualMachineTable,
|
||||
'table': VirtualMachineDetailTable,
|
||||
'url': 'virtualization:virtualmachine_list',
|
||||
}),
|
||||
))
|
||||
@@ -245,23 +246,3 @@ class APIRootView(APIView):
|
||||
('tenancy', reverse('tenancy-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."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
@@ -35,8 +35,22 @@ footer p {
|
||||
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) {
|
||||
#navbar_search {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse the nav menu on displays less than 960px wide */
|
||||
@media (max-width: 959px) {
|
||||
.navbar-header {
|
||||
float: none;
|
||||
}
|
||||
@@ -72,12 +86,8 @@ footer p {
|
||||
.collapse.in {
|
||||
display:block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide the nav search bar on displays less than 1600px wide */
|
||||
@media (max-width: 1599px) {
|
||||
#navbar_search {
|
||||
display: none;
|
||||
#navbar_user {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +116,14 @@ label {
|
||||
label.required {
|
||||
font-weight: bold;
|
||||
}
|
||||
input[name="pk"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
th.pk, td.pk {
|
||||
.table > tbody > tr > th.pk, .table > tbody > tr > td.pk {
|
||||
padding-bottom: 6px;
|
||||
padding-top: 10px;
|
||||
width: 30px;
|
||||
}
|
||||
tfoot td {
|
||||
@@ -390,6 +405,9 @@ table.reports td.stats label {
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.banner-bottom {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from django.http import HttpResponseBadRequest
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet, ViewSet
|
||||
|
||||
from django.http import HttpResponseBadRequest
|
||||
|
||||
from secrets import filters
|
||||
from secrets.exceptions import InvalidKey
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||
from . import serializers
|
||||
|
||||
|
||||
ERR_USERKEY_MISSING = "No UserKey found for the current user."
|
||||
ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
|
||||
ERR_PRIVKEY_MISSING = "Private key was not provided."
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Secret, SecretRole
|
||||
from dcim.models import Device
|
||||
from utilities.filters import NumericInFilter
|
||||
from .models import Secret, SecretRole
|
||||
|
||||
|
||||
class SecretRoleFilter(django_filters.FilterSet):
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
||||
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util import strxor
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django.contrib.auth.models import Group, User
|
||||
@@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
|
||||
super(Secret, self).__init__(*args, **kwargs)
|
||||
|
||||
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:
|
||||
return '{} for {}'.format(self.role, self.device)
|
||||
return 'Secret'
|
||||
@@ -300,6 +303,7 @@ class Secret(CreatedUpdatedModel):
|
||||
|LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|
||||
+--+--------+-------------------------------------------+
|
||||
"""
|
||||
s = s.encode('utf8')
|
||||
if len(s) > 65535:
|
||||
raise ValueError("Maximum plaintext size is 65535 bytes.")
|
||||
# Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
|
||||
@@ -312,7 +316,7 @@ class Secret(CreatedUpdatedModel):
|
||||
return (
|
||||
chr(len(s) >> 8).encode() +
|
||||
chr(len(s) % 256).encode() +
|
||||
s.encode() +
|
||||
s +
|
||||
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
|
||||
plaintext.
|
||||
"""
|
||||
if isinstance(s[0], int):
|
||||
plaintext_length = (s[0] << 8) + s[1]
|
||||
elif isinstance(s[0], str):
|
||||
if isinstance(s[0], str):
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -3,10 +3,8 @@ from __future__ import unicode_literals
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
{% 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>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
import base64
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
import base64
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
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 secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
# Dummy RSA key pair for testing use only
|
||||
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
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.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
|
||||
|
||||
|
||||
class UserKeyTestCase(TestCase):
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'secrets'
|
||||
urlpatterns = [
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
|
||||
from django.contrib import messages
|
||||
@@ -165,7 +166,7 @@ def secret_edit(request, pk):
|
||||
# Create and encrypt the new Secret
|
||||
if master_key is not None:
|
||||
secret = form.save(commit=False)
|
||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
||||
secret.plaintext = form.cleaned_data['plaintext']
|
||||
secret.encrypt(master_key)
|
||||
secret.save()
|
||||
messages.success(request, "Modified secret {}.".format(secret))
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<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-heading">
|
||||
<strong>
|
||||
@@ -21,13 +21,20 @@
|
||||
</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>There was a problem with your request. This error has been logged and administrative staff have
|
||||
been notified. Please return to the home page and try again.</p>
|
||||
<p>If you are responsible for this installation, please consider
|
||||
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
|
||||
information is provided below:</p>
|
||||
{% block message %}
|
||||
<p>
|
||||
There was a problem with your request. Please contact an administrator.
|
||||
</p>
|
||||
{% endblock %}
|
||||
<hr />
|
||||
<p>
|
||||
The complete exception is provided below:
|
||||
</p>
|
||||
<pre><strong>{{ exception }}</strong><br />
|
||||
{{ 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">
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<td>
|
||||
{% if circuit.tenant %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a circuit type
|
||||
</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 %}
|
||||
</div>
|
||||
<h1>{% block title %}Circuit Types{% endblock %}</h1>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device</strong>
|
||||
@@ -28,10 +28,10 @@
|
||||
<td>
|
||||
{% if device.rack %}
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@@ -42,7 +42,7 @@
|
||||
<td>
|
||||
{% if device.parent_bay %}
|
||||
{% 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 %}
|
||||
(U{{ parent.position }} / {{ parent.get_face_display }})
|
||||
{% endif %}
|
||||
@@ -61,7 +61,7 @@
|
||||
<td>
|
||||
{% if device.tenant %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||
@@ -172,6 +172,69 @@
|
||||
{% with device.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% 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 %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@@ -224,67 +287,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</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-heading">
|
||||
<strong>Images</strong>
|
||||
@@ -326,7 +328,9 @@
|
||||
{% endif %}
|
||||
</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 perms.dcim.delete_devicebay %}
|
||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||
@@ -350,7 +354,7 @@
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/devicebay.html' with selectable=True %}
|
||||
{% include 'dcim/inc/devicebay.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No device bays defined</td>
|
||||
@@ -405,11 +409,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 %}
|
||||
{% include 'dcim/inc/interface.html' with selectable=True %}
|
||||
{% include 'dcim/inc/interface.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No interfaces defined</td>
|
||||
<td colspan="8">No interfaces defined</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -467,8 +483,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 %}
|
||||
{% include 'dcim/inc/consoleserverport.html' with selectable=True %}
|
||||
{% include 'dcim/inc/consoleserverport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No console server ports defined</td>
|
||||
@@ -511,7 +535,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
<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">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
@@ -524,12 +548,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 %}
|
||||
{% include 'dcim/inc/poweroutlet.html' with selectable=True %}
|
||||
{% include 'dcim/inc/poweroutlet.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No power outlets defined</td>
|
||||
</tr>
|
||||
</tr> text-nowrap
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
|
||||
@@ -54,15 +54,27 @@ $(document).ready(function() {
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
var neighbor = neighbors[0];
|
||||
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1"));
|
||||
|
||||
// Glean configured hostnames/interfaces from the DOM
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||
if (configured_interface) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Clean up hostnames/interfaces learned via LLDP
|
||||
var lldp_device = neighbor['hostname'].split(".")[0]; // Strip off any trailing domain name
|
||||
var lldp_interface = neighbor['port'].split(".")[0]; // Strip off any trailing subinterface ID
|
||||
|
||||
// Add LLDP neighbors to table
|
||||
row.children('td.device').html(neighbor['hostname']);
|
||||
row.children('td.interface').html(neighbor['port']);
|
||||
row.children('td.device').html(lldp_device);
|
||||
row.children('td.interface').html(lldp_interface);
|
||||
|
||||
// Apply colors to rows
|
||||
if (!configured_device && neighbor['hostname']) {
|
||||
if (!configured_device && lldp_device) {
|
||||
row.addClass('info');
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port'].split(".")[0]) {
|
||||
} else if (configured_device == lldp_device && configured_interface == lldp_interface) {
|
||||
row.addClass('success');
|
||||
} else {
|
||||
row.addClass('danger');
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Bay</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device_bay.name }}</p>
|
||||
<p class="form-control-static">{{ device_bay }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a device role
|
||||
</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 %}
|
||||
</div>
|
||||
<h1>{% block title %}Device Roles{% endblock %}</h1>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<strong>Network Device</strong><br />
|
||||
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
|
||||
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} network interfaces</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Import device types
|
||||
</a>
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='devicetypes' %}
|
||||
{% include 'inc/export_button.html' with obj_type='device types' %}
|
||||
</div>
|
||||
<h1>{% block title %}Device Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||
</td>
|
||||
{% if cp.cs_port %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ cp.cs_port.name }}
|
||||
{{ cp.cs_port }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
<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">
|
||||
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if csp.connected_console %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ csp.connected_console.name }}
|
||||
{{ csp.connected_console }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td colspan="2" class="text-right">
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
{% if csp.connected_console %}
|
||||
{% if csp.connected_console.connection_status %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% endif %}
|
||||
{% if device.parent_bay %}
|
||||
<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 %}
|
||||
<li>{{ device }}</li>
|
||||
</ol>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<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">
|
||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<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>
|
||||
{% if devicebay.installed_device %}
|
||||
<td>
|
||||
|
||||
@@ -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 %}">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<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 }}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
</td>
|
||||
{% 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>
|
||||
<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 %}
|
||||
<span class="label label-primary">{{ iface.lag.name }}</span>
|
||||
{% endif %}
|
||||
{% if iface.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||
<a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>{{ iface.description }}</td>
|
||||
|
||||
{# MTU #}
|
||||
<td>{{ iface.mtu|default:"" }}</td>
|
||||
|
||||
{# MAC address #}
|
||||
<td>{{ iface.mac_address|default:"" }}</td>
|
||||
|
||||
{# Connection or type #}
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">
|
||||
LAG interface<br />
|
||||
@@ -55,7 +71,9 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td colspan="2" class="text-right">
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap">
|
||||
{% if show_graphs %}
|
||||
{% 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">
|
||||
@@ -96,7 +114,7 @@
|
||||
</a>
|
||||
{% 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 %}{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -106,62 +124,63 @@
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<a href="{% if iface.device %}{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with iface.ip_addresses.all as ipaddresses %}
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
<td colspan="6" class="subtable">
|
||||
{% else %}
|
||||
<td colspan="7" class="subtable">
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr class="ipaddress">
|
||||
|
||||
{# Placeholder #}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{# IP address #}
|
||||
<td colspan="2">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
</td>
|
||||
|
||||
{# Primary, status, role #}
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
<table class="table table-hover">
|
||||
{% for ip in ipaddresses %}
|
||||
<tr>
|
||||
<td>
|
||||
<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>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global table</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% 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">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
{% if ip.role %}
|
||||
<span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# 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>
|
||||
|
||||
{# Description #}
|
||||
<td colspan="2">
|
||||
{{ ip.description }}
|
||||
</td>
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap">
|
||||
{% 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">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>{{ item.manufacturer|default:"" }}</td>
|
||||
<td>{{ item.part_id }}</td>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
<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">
|
||||
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if po.connected_port %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ po.connected_port.name }}
|
||||
{{ po.connected_port }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td colspan="2" class="text-right">
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.change_poweroutlet %}
|
||||
{% if po.connected_port %}
|
||||
{% if po.connected_port.connection_status %}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||
</td>
|
||||
{% if pp.power_outlet %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ pp.power_outlet.name }}
|
||||
{{ pp.power_outlet }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a platform
|
||||
</a>
|
||||
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import platforms
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Platforms{% endblock %}</h1>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
|
||||
<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -72,7 +72,7 @@
|
||||
<td>Group</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@@ -93,7 +93,7 @@
|
||||
<td>
|
||||
{% if rack.tenant %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
|
||||
@@ -173,7 +173,7 @@
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Import rack groups
|
||||
</a>
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='rackgroups' %}
|
||||
{% include 'inc/export_button.html' with obj_type='rack groups' %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1>{% block title %}Rack Reservations{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %}
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a rack role
|
||||
</a>
|
||||
<a href="{% url 'dcim:rackrole_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import rack roles
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack Roles{% endblock %}</h1>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<td>
|
||||
{% if site.tenant %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<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>
|
||||
<p>Circuits</p>
|
||||
</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 class="panel panel-default">
|
||||
@@ -221,7 +225,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
{% for rg in rack_groups %}
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
18
netbox/templates/exceptions/import_error.html
Normal file
18
netbox/templates/exceptions/import_error.html
Normal 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 %}
|
||||
12
netbox/templates/exceptions/permission_error.html
Normal file
12
netbox/templates/exceptions/permission_error.html
Normal 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 %}
|
||||
17
netbox/templates/exceptions/programming_error.html
Normal file
17
netbox/templates/exceptions/programming_error.html
Normal 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 %}
|
||||
@@ -379,7 +379,9 @@
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="{{ request.user }}" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
{{ request.user|truncatechars:"30" }} <span class="caret"></span>
|
||||
<i class="fa fa-user"></i>
|
||||
<span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user