mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-08 18:09:34 +01:00
Compare commits
62 Commits
v2.5-beta1
...
v2.5-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1b42d466 | ||
|
|
7d8ae5e763 | ||
|
|
2bae50f501 | ||
|
|
a46f68c6e4 | ||
|
|
d59be2912e | ||
|
|
240d22696f | ||
|
|
2b1516ea79 | ||
|
|
89622f1ddf | ||
|
|
874acab90f | ||
|
|
34bfb899d1 | ||
|
|
55c153c5a9 | ||
|
|
c29ae9b785 | ||
|
|
5ce955a719 | ||
|
|
8c3a294384 | ||
|
|
55cc327e05 | ||
|
|
a324638f1f | ||
|
|
3366a6ae3d | ||
|
|
7dde370ee1 | ||
|
|
dfe6ba5603 | ||
|
|
fd9b2f2fda | ||
|
|
3c0181ef35 | ||
|
|
641254b23a | ||
|
|
63bd48003e | ||
|
|
23cde65add | ||
|
|
408f632636 | ||
|
|
83be0b5db4 | ||
|
|
7bed48f5fe | ||
|
|
2fce7ebd8f | ||
|
|
f8e6cfbeba | ||
|
|
fc41359df6 | ||
|
|
5649024d93 | ||
|
|
65bc8f0254 | ||
|
|
7887a70b70 | ||
|
|
8a6913fe19 | ||
|
|
7cd0e0b244 | ||
|
|
9543b5e716 | ||
|
|
bc8dbfde7c | ||
|
|
0c33af2140 | ||
|
|
b6a256dc5d | ||
|
|
5785fb6ba2 | ||
|
|
59589fdd29 | ||
|
|
75f0d8ee90 | ||
|
|
04ae6ec7af | ||
|
|
3bbf4a3352 | ||
|
|
0316072863 | ||
|
|
845d467fd9 | ||
|
|
be5bf6b711 | ||
|
|
788847edaa | ||
|
|
61ca7ee7c2 | ||
|
|
30f8fb4c11 | ||
|
|
3e92aa9fe7 | ||
|
|
bb5432de7d | ||
|
|
21fd889810 | ||
|
|
a228f1e1c2 | ||
|
|
4b5181d640 | ||
|
|
0dee55885b | ||
|
|
1e36a884fa | ||
|
|
d4e266d48c | ||
|
|
69d829ce8d | ||
|
|
c1838104ae | ||
|
|
c716ca1e87 | ||
|
|
c063961e4a |
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,4 +1,4 @@
|
||||
v2.5-beta1 (2018-11-06)
|
||||
v2.5-beta2 (2018-11-26)
|
||||
|
||||
## BETA RELEASE
|
||||
|
||||
@@ -31,11 +31,37 @@ NetBox now supports modeling physical cables for console, power, and interface c
|
||||
* [#1444](https://github.com/digitalocean/netbox/issues/1444) - Added an `asset_tag` field for racks
|
||||
* [#1931](https://github.com/digitalocean/netbox/issues/1931) - Added a count of assigned IP addresses to the interface API serializer
|
||||
* [#2000](https://github.com/digitalocean/netbox/issues/2000) - Dropped support for Python 2
|
||||
* [#2053](https://github.com/digitalocean/netbox/issues/2053) - Introduced the `LOGIN_TIMEOUT` configuration setting
|
||||
* [#2057](https://github.com/digitalocean/netbox/issues/2057) - Added description columns to interface connections list
|
||||
* [#2104](https://github.com/digitalocean/netbox/issues/2104) - Added a `status` field for racks
|
||||
* [#2165](https://github.com/digitalocean/netbox/issues/2165) - Improved natural ordering of Interfaces
|
||||
* [#2292](https://github.com/digitalocean/netbox/issues/2292) - Removed the deprecated UserAction model
|
||||
* [#2367](https://github.com/digitalocean/netbox/issues/2367) - Removed deprecated RPCClient functionality
|
||||
* [#2426](https://github.com/digitalocean/netbox/issues/2426) - Introduced `SESSION_FILE_PATH` configuration setting for authentication without write access to database
|
||||
|
||||
## Changes From v2.5-beta1
|
||||
|
||||
* [#2554](https://github.com/digitalocean/netbox/issues/2554) - Fix cable trace display when following a rear port with no cable attached
|
||||
* [#2563](https://github.com/digitalocean/netbox/issues/2563) - Enable export templates for cables
|
||||
* [#2566](https://github.com/digitalocean/netbox/issues/2566) - Prevent both ends of a cable from connecting to the same termination point
|
||||
* [#2567](https://github.com/digitalocean/netbox/issues/2567) - Introduced proxy models to represent console/power/interface connections
|
||||
* [#2569](https://github.com/digitalocean/netbox/issues/2569) - Added LSH fiber type; removed SC duplex/simplex designations
|
||||
* [#2570](https://github.com/digitalocean/netbox/issues/2570) - Add bulk disconnect view for front/rear pass-through ports
|
||||
* [#2571](https://github.com/digitalocean/netbox/issues/2571) - Enforce deletion of attached cable when deleting a termination point
|
||||
* [#2572](https://github.com/digitalocean/netbox/issues/2572) - Add button to disconnect cable from circuit termination
|
||||
* [#2573](https://github.com/digitalocean/netbox/issues/2573) - Fix bulk console/power/interface disconnections
|
||||
* [#2574](https://github.com/digitalocean/netbox/issues/2574) - Remove duplicate interface links from topology maps
|
||||
* [#2578](https://github.com/digitalocean/netbox/issues/2578) - Reorganized nested serializers
|
||||
* [#2579](https://github.com/digitalocean/netbox/issues/2579) - Add missing cable disconnect buttons for front/rear ports
|
||||
* [#2583](https://github.com/digitalocean/netbox/issues/2583) - Cleaned up component filters for device and device type
|
||||
* [#2584](https://github.com/digitalocean/netbox/issues/2584) - Prevent a Front port from being connected to its corresponding rear port
|
||||
* [#2585](https://github.com/digitalocean/netbox/issues/2585) - Prevent cable connections that include a virtual interface
|
||||
* [#2586](https://github.com/digitalocean/netbox/issues/2586) - Added tests for the Cable model's clean() method
|
||||
* [#2593](https://github.com/digitalocean/netbox/issues/2593) - Fix toggling of connected cable's status
|
||||
* [#2601](https://github.com/digitalocean/netbox/issues/2601) - Added a `description` field to pass-through ports
|
||||
* [#2602](https://github.com/digitalocean/netbox/issues/2602) - Return HTTP 204 when no new IPs/prefixes are available for provisioning
|
||||
* [#2608](https://github.com/digitalocean/netbox/issues/2608) - Fixed null `outer_unit` error on rack import
|
||||
* [#2609](https://github.com/digitalocean/netbox/issues/2609) - Fixed exception when ChoiceField integer value is passed as a string
|
||||
|
||||
## API Changes
|
||||
|
||||
@@ -55,6 +81,25 @@ NetBox now supports modeling physical cables for console, power, and interface c
|
||||
* The field `interface_ordering` has been removed from the DeviceType serializer
|
||||
* Added a `description` field to the CircuitTermination serializer
|
||||
* Added `ipaddress_count` to InterfaceSerializer to show the count of assigned IP addresses for each interface
|
||||
* The `available-prefixes` and `available-ips` IPAM endpoints now return an HTTP 204 response instead of HTTP 400 when no new objects can be created
|
||||
|
||||
---
|
||||
|
||||
v2.4.8 (2018-11-20)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2490](https://github.com/digitalocean/netbox/issues/2490) - Added bulk editing for config contexts
|
||||
* [#2557](https://github.com/digitalocean/netbox/issues/2557) - Added object view for tags
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets
|
||||
* [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed
|
||||
* [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables
|
||||
* [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table
|
||||
* [#2588](https://github.com/digitalocean/netbox/issues/2588) - Catch all exceptions from failed NAPALM API Calls
|
||||
* [#2589](https://github.com/digitalocean/netbox/issues/2589) - Virtual machine API serializer should require cluster assignment
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -133,6 +133,14 @@ Setting this to True will permit only authenticated users to access any part of
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_TIMEOUT
|
||||
|
||||
Default: 1209600 seconds (14 days)
|
||||
|
||||
The liftetime (in seconds) of the authentication cookie issued to a NetBox user upon login.
|
||||
|
||||
---
|
||||
|
||||
## MAINTENANCE_MODE
|
||||
|
||||
Default: False
|
||||
@@ -223,6 +231,14 @@ The file path to the location where custom reports will be kept. By default, thi
|
||||
|
||||
---
|
||||
|
||||
## SESSION_FILE_PATH
|
||||
|
||||
Default: None
|
||||
|
||||
Session data is used to track authenticated users when they access NetBox. By default, NetBox stores session data in the PostgreSQL database. However, this inhibits authentication to a standby instance of NetBox without write access to the database. Alternatively, a local file path may be specified here and NetBox will store session data as files instead of using the database. Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
70
docs/development/extending-models.md
Normal file
70
docs/development/extending-models.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Extending Models
|
||||
|
||||
Below is a list of items to consider when adding a new field to a model:
|
||||
|
||||
### 1. Generate and run database migration
|
||||
|
||||
Django migrations are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
|
||||
|
||||
```
|
||||
./manage.py makemigrations <app> -n <name>
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
Where possible, try to merge related changes into a single migration. For example, if three new fields are being added to different models within an app, these can be expressed in the same migration. You can merge a new migration with an existing one by combining their `operations` lists.
|
||||
|
||||
!!! note
|
||||
Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered.
|
||||
|
||||
### 2. Add validation logic to `clean()`
|
||||
|
||||
If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or agter your custom validation as appropriate:
|
||||
|
||||
```
|
||||
class Foo(models.Model):
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(DeviceCSVForm, self).clean()
|
||||
|
||||
# Custom validation goes here
|
||||
if self.bar is None:
|
||||
raise ValidationError()
|
||||
```
|
||||
|
||||
### 3. Add CSV helpers
|
||||
|
||||
Add the name of the new field to `csv_headers` and included a CSV-friendly representation of its data in the model's `to_csv()` method. These will be used when exporting objects in CSV format.
|
||||
|
||||
### 4. Update relevant querysets
|
||||
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `select_related()` or `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups.
|
||||
|
||||
### 5. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
|
||||
|
||||
### 6. Add field to forms
|
||||
|
||||
Extend any forms to include the new field as appropriate. Common forms include:
|
||||
|
||||
* **Credit/edit** - Manipulating a single object
|
||||
* **Bulk edit** - Performing a change on mnay objects at once
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
### 7. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
|
||||
|
||||
### 8. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column.
|
||||
|
||||
### 9. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
### 10. Adjust API and model tests
|
||||
|
||||
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.
|
||||
@@ -28,10 +28,3 @@ NetBox components are arranged into functional subsections called _apps_ (a carr
|
||||
* `tenancy`: Tenants (such as customers) to which NetBox objects may be assigned
|
||||
* `utilities`: Resources which are not user-facing (extendable classes, etc.)
|
||||
* `virtualization`: Virtual machines and clusters
|
||||
|
||||
## Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). The following exceptions are noted:
|
||||
|
||||
* [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
|
||||
* Constants may be imported via wildcard (for example, `from .constants import *`).
|
||||
|
||||
41
docs/development/style-guide.md
Normal file
41
docs/development/style-guide.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
|
||||
|
||||
## PEP 8 Exceptions
|
||||
|
||||
* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
* The library being import contains only constant declarations (`constants.py`)
|
||||
* The library being imported explicitly defines `__all__` (e.g. `<app>.api.nested_serializers`)
|
||||
|
||||
* Maximum line length is 120 characters (E501)
|
||||
* This does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
* Line breaks are permitted following binary operators (W504)
|
||||
|
||||
## Enforcing Code Style
|
||||
|
||||
The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails.
|
||||
|
||||
```
|
||||
$ cd .git/hooks/
|
||||
$ ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
|
||||
To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
|
||||
## General Guidance
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
|
||||
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
|
||||
|
||||
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
|
||||
* Every model should have a docstring. Every custom method should include an expalantion of its function.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
@@ -45,7 +45,9 @@ pages:
|
||||
- Examples: 'api/examples.md'
|
||||
- Development:
|
||||
- Introduction: 'development/index.md'
|
||||
- Style Guide: 'development/style-guide.md'
|
||||
- Utility Views: 'development/utility-views.md'
|
||||
- Extending Models: 'development/extending-models.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
|
||||
markdown_extensions:
|
||||
|
||||
52
netbox/circuits/api/nested_serializers.py
Normal file
52
netbox/circuits/api/nested_serializers.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
'NestedCircuitTerminationSerializer',
|
||||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'circuit', 'term_side']
|
||||
@@ -1,12 +1,13 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import ConnectedEndpointSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, ValidatedModelSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
@@ -24,16 +25,8 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
@@ -43,18 +36,6 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||
@@ -70,36 +51,14 @@ class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
|
||||
|
||||
#
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationSerializer(ValidatedModelSerializer):
|
||||
class CircuitTerminationSerializer(ConnectedEndpointSerializer):
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
connected_endpoint = NestedInterfaceSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'connected_endpoint', 'cable',
|
||||
'description', 'connected_endpoint', 'connection_status', 'cable',
|
||||
]
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'circuit', 'term_side']
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 NumericInFilter
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
@@ -29,9 +29,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@@ -110,9 +108,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
|
||||
@@ -70,7 +70,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(default=True),
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
|
||||
@@ -237,7 +237,7 @@ class CircuitTermination(CableTermination):
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
default=CONNECTION_STATUS_CONNECTED
|
||||
blank=True
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name='Port speed (Kbps)'
|
||||
|
||||
243
netbox/dcim/api/nested_serializers.py
Normal file
243
netbox/dcim/api/nested_serializers.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
|
||||
Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
|
||||
Region, Site, VirtualChassis,
|
||||
)
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
'NestedConsolePortSerializer',
|
||||
'NestedConsoleServerPortSerializer',
|
||||
'NestedDeviceBaySerializer',
|
||||
'NestedDeviceRoleSerializer',
|
||||
'NestedDeviceSerializer',
|
||||
'NestedDeviceTypeSerializer',
|
||||
'NestedFrontPortSerializer',
|
||||
'NestedFrontPortTemplateSerializer',
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedManufacturerSerializer',
|
||||
'NestedPlatformSerializer',
|
||||
'NestedPowerOutletSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedRackGroupSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
'NestedRackSerializer',
|
||||
'NestedRearPortSerializer',
|
||||
'NestedRearPortTemplateSerializer',
|
||||
'NestedRegionSerializer',
|
||||
'NestedSiteSerializer',
|
||||
'NestedVirtualChassisSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class NestedCableSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'url', 'label']
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'master']
|
||||
@@ -2,7 +2,6 @@ from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@@ -11,28 +10,40 @@ from dcim.models import (
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
|
||||
WritableNestedSerializer, get_serializer_for_model,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
def get_connected_endpoint(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
"""
|
||||
if getattr(obj, 'connected_endpoint', None) is None:
|
||||
return None
|
||||
|
||||
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.connected_endpoint, context=context).data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class RegionSerializer(serializers.ModelSerializer):
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
|
||||
@@ -41,10 +52,6 @@ class RegionSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'parent']
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
@@ -62,16 +69,8 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackGroupSerializer(ValidatedModelSerializer):
|
||||
@@ -82,18 +81,6 @@ class RackGroupSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'site']
|
||||
|
||||
|
||||
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -101,18 +88,6 @@ class RackRoleSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
@@ -151,26 +126,6 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
# Rack units
|
||||
#
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
|
||||
|
||||
class RackUnitSerializer(serializers.Serializer):
|
||||
"""
|
||||
A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database.
|
||||
@@ -181,10 +136,6 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationSerializer(ValidatedModelSerializer):
|
||||
rack = NestedRackSerializer()
|
||||
user = NestedUserSerializer()
|
||||
@@ -196,7 +147,7 @@ class RackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
# Device types
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
@@ -206,18 +157,6 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||
@@ -232,19 +171,6 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Console port templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
|
||||
@@ -253,10 +179,6 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Console server port templates
|
||||
#
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
|
||||
@@ -265,10 +187,6 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power port templates
|
||||
#
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
|
||||
@@ -277,10 +195,6 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power outlet templates
|
||||
#
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
|
||||
@@ -289,10 +203,6 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Interface templates
|
||||
#
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
@@ -302,10 +212,6 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
#
|
||||
# Rear port templates
|
||||
#
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
@@ -315,18 +221,6 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Front port templates
|
||||
#
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
@@ -337,18 +231,6 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Device bay templates
|
||||
#
|
||||
|
||||
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
|
||||
@@ -358,7 +240,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
@@ -368,18 +250,6 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformSerializer(ValidatedModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||
|
||||
@@ -388,46 +258,6 @@ class PlatformSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class DeviceIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
|
||||
|
||||
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'master']
|
||||
|
||||
|
||||
class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
@@ -437,12 +267,12 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
parent_device = serializers.SerializerMethodField()
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -450,8 +280,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
fields = [
|
||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'local_context_data',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@@ -486,14 +316,161 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
fields = [
|
||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
|
||||
'config_context', 'created', 'last_updated', 'local_context_data',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
|
||||
'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
serializer=NestedVLANSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
'count_ipaddresses',
|
||||
]
|
||||
|
||||
# TODO: This validation should be handled by Interface.clean()
|
||||
def validate(self, data):
|
||||
|
||||
# All associated VLANs be global or assigned to the parent device's site.
|
||||
device = self.instance.device if self.instance else data.get('device')
|
||||
untagged_vlan = data.get('untagged_vlan')
|
||||
if untagged_vlan and untagged_vlan.site not in [device.site, None]:
|
||||
raise serializers.ValidationError({
|
||||
'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
|
||||
"global.".format(untagged_vlan)
|
||||
})
|
||||
for vlan in data.get('tagged_vlans', []):
|
||||
if vlan.site not in [device.site, None]:
|
||||
raise serializers.ValidationError({
|
||||
'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
|
||||
"be global.".format(vlan)
|
||||
})
|
||||
|
||||
return super(InterfaceSerializer, self).validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'device', 'name', 'type', 'positions', 'description', 'cable', 'tags']
|
||||
|
||||
|
||||
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
"""
|
||||
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
||||
"""
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class FrontPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
|
||||
|
||||
|
||||
class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'device', 'name', 'installed_device', 'tags']
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
@@ -548,305 +525,6 @@ class TracedCableSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedCableSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'url', 'label']
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'cable', 'tags']
|
||||
read_only_fields = ['connected_endpoint']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
connected_endpoint = NestedConsoleServerPortSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'cable', 'tags']
|
||||
read_only_fields = ['connected_endpoint']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
connected_endpoint = NestedPowerOutletSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
|
||||
|
||||
class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
circuit = InterfaceNestedCircuitSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
]
|
||||
|
||||
|
||||
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
|
||||
class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
serializer=InterfaceVLANSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# All associated VLANs be global or assigned to the parent device's site.
|
||||
device = self.instance.device if self.instance else data.get('device')
|
||||
untagged_vlan = data.get('untagged_vlan')
|
||||
if untagged_vlan and untagged_vlan.site not in [device.site, None]:
|
||||
raise serializers.ValidationError({
|
||||
'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
|
||||
"global.".format(untagged_vlan)
|
||||
})
|
||||
for vlan in data.get('tagged_vlans', []):
|
||||
if vlan.site not in [device.site, None]:
|
||||
raise serializers.ValidationError({
|
||||
'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
|
||||
"be global.".format(vlan)
|
||||
})
|
||||
|
||||
return super(InterfaceSerializer, self).validate(data)
|
||||
|
||||
def get_connected_endpoint(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
"""
|
||||
if obj.connected_endpoint is None:
|
||||
return None
|
||||
|
||||
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.connected_endpoint, context=context).data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# Rear ports
|
||||
#
|
||||
|
||||
class RearPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'device', 'name', 'type', 'positions', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Front ports
|
||||
#
|
||||
|
||||
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class FrontPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
||||
class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'device', 'name', 'installed_device', 'tags']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
@@ -876,11 +554,3 @@ class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'master', 'domain', 'tags']
|
||||
|
||||
|
||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url']
|
||||
|
||||
@@ -309,9 +309,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
# Check that NAPALM is installed
|
||||
try:
|
||||
import napalm
|
||||
from napalm.base.exceptions import ModuleImportError
|
||||
except ImportError:
|
||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||
from napalm.base.exceptions import ModuleImportError
|
||||
|
||||
# Validate the configured driver
|
||||
try:
|
||||
@@ -355,7 +355,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
try:
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
|
||||
d.close()
|
||||
|
||||
return Response(response)
|
||||
|
||||
@@ -226,12 +226,12 @@ IFACE_MODE_CHOICES = [
|
||||
# Pass-through port types
|
||||
PORT_TYPE_8P8C = 1000
|
||||
PORT_TYPE_ST = 2000
|
||||
PORT_TYPE_SC_SIMPLEX = 2100
|
||||
PORT_TYPE_SC_DUPLEX = 2110
|
||||
PORT_TYPE_SC = 2100
|
||||
PORT_TYPE_FC = 2200
|
||||
PORT_TYPE_LC = 2300
|
||||
PORT_TYPE_MTRJ = 2400
|
||||
PORT_TYPE_MPO = 2500
|
||||
PORT_TYPE_LSH = 2600
|
||||
PORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Copper',
|
||||
@@ -242,13 +242,13 @@ PORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Fiber Optic',
|
||||
[
|
||||
[PORT_TYPE_ST, 'ST'],
|
||||
[PORT_TYPE_SC_SIMPLEX, 'SC (Simplex)'],
|
||||
[PORT_TYPE_SC_DUPLEX, 'SC (Duplex)'],
|
||||
[PORT_TYPE_FC, 'FC'],
|
||||
[PORT_TYPE_LC, 'LC'],
|
||||
[PORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PORT_TYPE_LSH, 'LSH'],
|
||||
[PORT_TYPE_MPO, 'MPO'],
|
||||
[PORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PORT_TYPE_SC, 'SC'],
|
||||
[PORT_TYPE_ST, 'ST'],
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
|
||||
from virtualization.models import Cluster
|
||||
from .constants import *
|
||||
from .models import (
|
||||
@@ -81,9 +81,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -202,9 +200,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Role (slug)',
|
||||
)
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
@@ -303,7 +299,7 @@ class ManufacturerFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class DeviceTypeFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -322,33 +318,31 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
console_ports = django_filters.CharFilter(
|
||||
console_ports = django_filters.BooleanFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
)
|
||||
console_server_ports = django_filters.CharFilter(
|
||||
console_server_ports = django_filters.BooleanFilter(
|
||||
method='_console_server_ports',
|
||||
label='Has console server ports',
|
||||
)
|
||||
power_ports = django_filters.CharFilter(
|
||||
power_ports = django_filters.BooleanFilter(
|
||||
method='_power_ports',
|
||||
label='Has power ports',
|
||||
)
|
||||
power_outlets = django_filters.CharFilter(
|
||||
power_outlets = django_filters.BooleanFilter(
|
||||
method='_power_outlets',
|
||||
label='Has power outlets',
|
||||
)
|
||||
interfaces = django_filters.CharFilter(
|
||||
interfaces = django_filters.BooleanFilter(
|
||||
method='_interfaces',
|
||||
label='Has interfaces',
|
||||
)
|
||||
pass_through_ports = django_filters.CharFilter(
|
||||
pass_through_ports = django_filters.BooleanFilter(
|
||||
method='_pass_through_ports',
|
||||
label='Has pass-through ports',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
@@ -367,30 +361,24 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleport_templates__isnull=bool(value))
|
||||
return queryset.exclude(consoleport_templates__isnull=value)
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleserverport_templates__isnull=bool(value))
|
||||
return queryset.exclude(consoleserverport_templates__isnull=value)
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(powerport_templates__isnull=bool(value))
|
||||
return queryset.exclude(powerport_templates__isnull=value)
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(poweroutlet_templates__isnull=bool(value))
|
||||
return queryset.exclude(poweroutlet_templates__isnull=value)
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(interface_templates__isnull=bool(value))
|
||||
return queryset.exclude(interface_templates__isnull=value)
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(
|
||||
frontport_templates__isnull=bool(value),
|
||||
rearport_templates__isnull=bool(value)
|
||||
frontport_templates__isnull=value,
|
||||
rearport_templates__isnull=value
|
||||
)
|
||||
|
||||
|
||||
@@ -483,7 +471,7 @@ class PlatformFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class DeviceFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -588,30 +576,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
field_name='device_type__is_full_depth',
|
||||
label='Is full depth',
|
||||
)
|
||||
console_ports = django_filters.CharFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
)
|
||||
console_server_ports = django_filters.CharFilter(
|
||||
method='_console_server_ports',
|
||||
label='Has console server ports',
|
||||
)
|
||||
power_ports = django_filters.CharFilter(
|
||||
method='_power_ports',
|
||||
label='Has power ports',
|
||||
)
|
||||
power_outlets = django_filters.CharFilter(
|
||||
method='_power_outlets',
|
||||
label='Has power outlets',
|
||||
)
|
||||
interfaces = django_filters.CharFilter(
|
||||
method='_interfaces',
|
||||
label='Has interfaces',
|
||||
)
|
||||
pass_through_ports = django_filters.CharFilter(
|
||||
method='_pass_through_ports',
|
||||
label='Has pass-through ports',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
@@ -625,9 +589,31 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual chassis (ID)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
console_ports = django_filters.BooleanFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
)
|
||||
console_server_ports = django_filters.BooleanFilter(
|
||||
method='_console_server_ports',
|
||||
label='Has console server ports',
|
||||
)
|
||||
power_ports = django_filters.BooleanFilter(
|
||||
method='_power_ports',
|
||||
label='Has power ports',
|
||||
)
|
||||
power_outlets = django_filters.BooleanFilter(
|
||||
method='_power_outlets',
|
||||
label='Has power outlets',
|
||||
)
|
||||
interfaces = django_filters.BooleanFilter(
|
||||
method='_interfaces',
|
||||
label='Has interfaces',
|
||||
)
|
||||
pass_through_ports = django_filters.BooleanFilter(
|
||||
method='_pass_through_ports',
|
||||
label='Has pass-through ports',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@@ -677,30 +663,24 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleports__isnull=bool(value))
|
||||
return queryset.exclude(consoleports__isnull=value)
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleserverports__isnull=bool(value))
|
||||
return queryset.exclude(consoleserverports__isnull=value)
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(powerports__isnull=bool(value))
|
||||
return queryset.exclude(powerports__isnull=value)
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(poweroutlets__isnull=bool(value))
|
||||
return queryset.exclude(poweroutlets_isnull=value)
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(interfaces__isnull=bool(value))
|
||||
return queryset.exclude(interfaces__isnull=value)
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(
|
||||
frontports__isnull=bool(value),
|
||||
rearports__isnull=bool(value)
|
||||
frontports__isnull=value,
|
||||
rearports__isnull=value
|
||||
)
|
||||
|
||||
|
||||
@@ -714,9 +694,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
@@ -775,9 +753,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
label='Assigned VLAN'
|
||||
@@ -932,9 +908,7 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
|
||||
@@ -19,7 +19,7 @@ from utilities.forms import (
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||
ConfirmationForm, ContentTypeSelect, CSVChoiceField, ExpandableNameField, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, JSONField, Livesearch, SelectWithPK, SmallTextarea,
|
||||
SlugField, COLOR_CHOICES,
|
||||
SlugField, BOOLEAN_WITH_BLANK_CHOICES, COLOR_CHOICES,
|
||||
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
@@ -650,35 +650,41 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
console_ports = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has console ports'
|
||||
)
|
||||
console_server_ports = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has console server ports'
|
||||
)
|
||||
power_ports = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has power ports'
|
||||
)
|
||||
power_outlets = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has power outlets'
|
||||
)
|
||||
interfaces = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has interfaces'
|
||||
)
|
||||
pass_through_ports = forms.BooleanField(
|
||||
required=False,
|
||||
label='Has pass-through ports'
|
||||
)
|
||||
subdevice_role = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Subdevice role',
|
||||
widget=forms.Select(choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES))
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
console_server_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console server ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
power_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has power ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
power_outlets = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has power outlets',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
interfaces = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has interfaces',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
pass_through_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has pass-through ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -1316,11 +1322,37 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a primary IP',
|
||||
widget=forms.Select(choices=[
|
||||
('', '---------'),
|
||||
('True', 'Yes'),
|
||||
('False', 'No'),
|
||||
])
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
console_server_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console server ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
power_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has power ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
power_outlets = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has power outlets',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
interfaces = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has interfaces',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
pass_through_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has pass-through ports',
|
||||
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
)
|
||||
|
||||
|
||||
@@ -1653,13 +1685,13 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'tags']
|
||||
fields = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
||||
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
||||
class FrontPortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
@@ -1672,6 +1704,9 @@ class FrontPortCreateForm(ComponentForm):
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.'
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1723,6 +1758,13 @@ class FrontPortBulkRenameForm(BulkRenameForm):
|
||||
)
|
||||
|
||||
|
||||
class FrontPortBulkDisconnectForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Rear pass-through ports
|
||||
#
|
||||
@@ -1732,7 +1774,7 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['device', 'name', 'type', 'positions', 'tags']
|
||||
fields = ['device', 'name', 'type', 'positions', 'description', 'tags']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
@@ -1751,6 +1793,9 @@ class RearPortCreateForm(ComponentForm):
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class RearPortBulkRenameForm(BulkRenameForm):
|
||||
@@ -1760,6 +1805,13 @@ class RearPortBulkRenameForm(BulkRenameForm):
|
||||
)
|
||||
|
||||
|
||||
class RearPortBulkDisconnectForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
@@ -1819,7 +1871,10 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
label='Name',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/{{termination_b_type}}s/?device_id={{termination_b_device}}',
|
||||
disabled_indicator='cable'
|
||||
disabled_indicator='cable',
|
||||
url_conditional_append={
|
||||
'termination_b_type__interface': '&type=physical',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1837,6 +1892,8 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
termination_a_type = self.instance.termination_a._meta.model_name
|
||||
self.fields['termination_b_type'].queryset = ContentType.objects.filter(
|
||||
model__in=COMPATIBLE_TERMINATION_TYPES.get(termination_a_type)
|
||||
).exclude(
|
||||
model='circuittermination'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
@@ -44,6 +45,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.Device')),
|
||||
('tags', taggit.managers.TaggableManager(through='taggit.TaggedItem', to='taggit.Tag')),
|
||||
],
|
||||
|
||||
@@ -34,8 +34,13 @@ def console_connections_to_cables(apps, schema_editor):
|
||||
)
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
ConsolePort.objects.filter(pk=consoleport.id).update(cable=cable)
|
||||
ConsoleServerPort.objects.filter(pk=consoleport.connected_endpoint_id).update(cable=cable)
|
||||
ConsolePort.objects.filter(pk=consoleport.id).update(
|
||||
cable=cable
|
||||
)
|
||||
ConsoleServerPort.objects.filter(pk=consoleport.connected_endpoint_id).update(
|
||||
connection_status=consoleport.connection_status,
|
||||
cable=cable
|
||||
)
|
||||
|
||||
cable_count = Cable.objects.filter(termination_a_type=consoleport_type).count()
|
||||
if 'test' not in sys.argv:
|
||||
@@ -70,8 +75,13 @@ def power_connections_to_cables(apps, schema_editor):
|
||||
)
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
PowerPort.objects.filter(pk=powerport.id).update(cable=cable)
|
||||
PowerOutlet.objects.filter(pk=powerport.connected_endpoint_id).update(cable=cable)
|
||||
PowerPort.objects.filter(pk=powerport.id).update(
|
||||
cable=cable
|
||||
)
|
||||
PowerOutlet.objects.filter(pk=powerport.connected_endpoint_id).update(
|
||||
connection_status=powerport.connection_status,
|
||||
cable=cable
|
||||
)
|
||||
|
||||
cable_count = Cable.objects.filter(termination_a_type=powerport_type).count()
|
||||
if 'test' not in sys.argv:
|
||||
@@ -174,6 +184,11 @@ class Migration(migrations.Migration):
|
||||
name='connected_endpoint',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_endpoint', to='dcim.ConsoleServerPort'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cable',
|
||||
@@ -189,6 +204,11 @@ class Migration(migrations.Migration):
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
|
||||
# Alter power port models
|
||||
migrations.RenameField(
|
||||
@@ -206,6 +226,11 @@ class Migration(migrations.Migration):
|
||||
name='connected_endpoint',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_endpoint', to='dcim.PowerOutlet'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='cable',
|
||||
@@ -221,6 +246,11 @@ class Migration(migrations.Migration):
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
|
||||
# Alter the Interface model
|
||||
migrations.AddField(
|
||||
@@ -236,7 +266,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(default=True),
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
@@ -274,4 +304,35 @@ class Migration(migrations.Migration):
|
||||
name='InterfaceConnection',
|
||||
),
|
||||
|
||||
# Proxy models
|
||||
migrations.CreateModel(
|
||||
name='ConsoleConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.consoleport',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.interface',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.powerport',),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -73,10 +73,22 @@ class CableTermination(models.Model):
|
||||
null=True
|
||||
)
|
||||
|
||||
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
|
||||
_cabled_as_a = GenericRelation(
|
||||
to='dcim.Cable',
|
||||
content_type_field='termination_a_type',
|
||||
object_id_field='termination_a_id'
|
||||
)
|
||||
_cabled_as_b = GenericRelation(
|
||||
to='dcim.Cable',
|
||||
content_type_field='termination_b_type',
|
||||
object_id_field='termination_b_id'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self, position=1):
|
||||
def trace(self, position=1, follow_circuits=False):
|
||||
"""
|
||||
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
||||
[
|
||||
@@ -85,7 +97,7 @@ class CableTermination(models.Model):
|
||||
(termination E, cable, termination F)
|
||||
]
|
||||
"""
|
||||
def get_peer_port(termination, position=1):
|
||||
def get_peer_port(termination, position=1, follow_circuits=False):
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
# Map a front port to its corresponding rear port
|
||||
@@ -105,7 +117,7 @@ class CableTermination(models.Model):
|
||||
return peer_port, 1
|
||||
|
||||
# Follow a circuit to its other termination
|
||||
elif isinstance(termination, CircuitTermination):
|
||||
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if peer_termination is None:
|
||||
return None, None
|
||||
@@ -121,7 +133,7 @@ class CableTermination(models.Model):
|
||||
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
||||
path = [(self, self.cable, far_end)]
|
||||
|
||||
peer_port, position = get_peer_port(far_end, position)
|
||||
peer_port, position = get_peer_port(far_end, position, follow_circuits)
|
||||
if peer_port is None:
|
||||
return path
|
||||
|
||||
@@ -540,12 +552,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def clean(self):
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
if self.outer_width and not self.outer_unit:
|
||||
raise ValidationError("Must specify a unit when setting an outer width")
|
||||
if self.outer_depth and not self.outer_unit:
|
||||
raise ValidationError("Must specify a unit when setting an outer depth")
|
||||
if self.outer_unit and self.outer_width is None and self.outer_depth is None:
|
||||
self.length_unit = ''
|
||||
if (self.outer_width or self.outer_depth) and not self.outer_unit:
|
||||
raise ValidationError("Must specify a unit when setting an outer width/depth")
|
||||
else:
|
||||
self.outer_unit = ''
|
||||
|
||||
if self.pk:
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
@@ -1692,13 +1702,13 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
default=CONNECTION_STATUS_CONNECTED
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['console_server', 'connected_endpoint', 'device', 'console_port', 'connection_status']
|
||||
csv_headers = ['device', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
@@ -1712,11 +1722,8 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1736,10 +1743,16 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
@@ -1749,6 +1762,12 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
@@ -1775,13 +1794,13 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
default=CONNECTION_STATUS_CONNECTED
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['pdu', 'connected_endpoint', 'device', 'power_port', 'connection_status']
|
||||
csv_headers = ['device', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
@@ -1795,11 +1814,8 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1819,10 +1835,16 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
@@ -1832,6 +1854,12 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
@@ -1875,7 +1903,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
default=CONNECTION_STATUS_CONNECTED
|
||||
blank=True
|
||||
)
|
||||
lag = models.ForeignKey(
|
||||
to='self',
|
||||
@@ -1935,6 +1963,11 @@ class Interface(CableTermination, ComponentModel):
|
||||
objects = InterfaceManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = [
|
||||
'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
||||
'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1945,6 +1978,21 @@ class Interface(CableTermination, ComponentModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:interface', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.name,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_form_factor_display(),
|
||||
self.enabled,
|
||||
self.mac_address,
|
||||
self.mtu,
|
||||
self.mgmt_only,
|
||||
self.description,
|
||||
self.get_mode_display(),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# An Interface must belong to a Device *or* to a VirtualMachine
|
||||
@@ -2108,10 +2156,16 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = [
|
||||
@@ -2122,6 +2176,16 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_type_display(),
|
||||
self.rear_port.name,
|
||||
self.rear_port_position,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate rear port assignment
|
||||
@@ -2158,10 +2222,16 @@ class RearPort(CableTermination, ComponentModel):
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -2169,6 +2239,15 @@ class RearPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_type_display(),
|
||||
self.positions,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
@@ -2198,6 +2277,8 @@ class DeviceBay(ComponentModel):
|
||||
objects = DeviceComponentManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'installed_device']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -2208,6 +2289,13 @@ class DeviceBay(ComponentModel):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.installed_device.identifier if self.installed_device else None,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
@@ -2456,6 +2544,21 @@ class Cable(ChangeLoggedModel):
|
||||
self.termination_a_type, self.termination_b_type
|
||||
))
|
||||
|
||||
# A termination point cannot be connected to itself
|
||||
if self.termination_a == self.termination_b:
|
||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||
|
||||
# A front port cannot be connected to its corresponding rear port
|
||||
if (
|
||||
type_a in ['frontport', 'rearport'] and
|
||||
type_b in ['frontport', 'rearport'] and
|
||||
(
|
||||
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
||||
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
||||
)
|
||||
):
|
||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
||||
|
||||
# Check for an existing Cable connected to either termination object
|
||||
if self.termination_a.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
@@ -2466,6 +2569,20 @@ class Cable(ChangeLoggedModel):
|
||||
self.termination_b, self.termination_b.cable_id
|
||||
))
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
||||
if (
|
||||
(
|
||||
isinstance(endpoint_a, Interface) and
|
||||
endpoint_a.form_factor == IFACE_FF_VIRTUAL
|
||||
) or
|
||||
(
|
||||
isinstance(endpoint_b, Interface) and
|
||||
endpoint_b.form_factor == IFACE_FF_VIRTUAL
|
||||
)
|
||||
):
|
||||
raise ValidationError("Cannot connect to a virtual interface")
|
||||
|
||||
# Validate length and length_unit
|
||||
if self.length and not self.length_unit:
|
||||
raise ValidationError("Must specify a unit when setting a cable length")
|
||||
@@ -2499,39 +2616,79 @@ class Cable(ChangeLoggedModel):
|
||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||
None.
|
||||
"""
|
||||
def trace_cable(termination, position=1):
|
||||
a_path = self.termination_b.trace()
|
||||
b_path = self.termination_a.trace()
|
||||
|
||||
# Given a front port, follow the cable connected to the corresponding rear port/position
|
||||
if isinstance(termination, FrontPort):
|
||||
peer_port = termination.rear_port
|
||||
position = termination.rear_port_position
|
||||
# Determine overall path status (connected or planned)
|
||||
if self.status == CONNECTION_STATUS_PLANNED:
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
path_status = CONNECTION_STATUS_CONNECTED
|
||||
for segment in a_path[1:] + b_path[1:]:
|
||||
if segment[1] is None or segment[1].status == CONNECTION_STATUS_PLANNED:
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
break
|
||||
|
||||
# Given a rear port/position, follow the cable connected to the corresponding front port
|
||||
elif isinstance(termination, RearPort):
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
position = 1
|
||||
# (A path end, B path end, connected/planned)
|
||||
return a_path[-1][2], b_path[-1][2], path_status
|
||||
|
||||
# Termination is not a pass-through port, so we've reached the end of the path
|
||||
else:
|
||||
return termination
|
||||
|
||||
# Find the cable (if any) attached to the peer port
|
||||
next_cable = peer_port.cable
|
||||
#
|
||||
# Connection proxy models
|
||||
#
|
||||
|
||||
# If no cable exists, return None
|
||||
if next_cable is None:
|
||||
return None
|
||||
class ConsoleConnection(ConsolePort):
|
||||
|
||||
far_end = next_cable.termination_b if next_cable.termination_a == peer_port else next_cable.termination_a
|
||||
csv_headers = [
|
||||
'console_server', 'port', 'device', 'console_port', 'connection_status',
|
||||
]
|
||||
|
||||
# Return the far side termination of the cable
|
||||
return trace_cable(far_end, position)
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
return trace_cable(self.termination_a), trace_cable(self.termination_b)
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
class PowerConnection(PowerPort):
|
||||
|
||||
csv_headers = [
|
||||
'pdu', 'outlet', 'device', 'power_port', 'connection_status',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnection(Interface):
|
||||
|
||||
csv_headers = [
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
@@ -23,35 +23,45 @@ def clear_virtualchassis_members(instance, **kwargs):
|
||||
|
||||
@receiver(post_save, sender=Cable)
|
||||
def update_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
When a Cable is saved, check for and update its two connected endpoints
|
||||
"""
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
instance.termination_a.cable = instance
|
||||
instance.termination_a.save()
|
||||
instance.termination_b.cable = instance
|
||||
instance.termination_b.save()
|
||||
if instance.termination_a.cable != instance:
|
||||
instance.termination_a.cable = instance
|
||||
instance.termination_a.save()
|
||||
if instance.termination_b.cable != instance:
|
||||
instance.termination_b.cable = instance
|
||||
instance.termination_b.save()
|
||||
|
||||
# Check if this Cable has formed a complete path. If so, update both endpoints.
|
||||
endpoint_a, endpoint_b = instance.get_path_endpoints()
|
||||
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
|
||||
if endpoint_a is not None and endpoint_b is not None:
|
||||
endpoint_a.connected_endpoint = endpoint_b
|
||||
endpoint_a.connection_status = True
|
||||
endpoint_a.connection_status = path_status
|
||||
endpoint_a.save()
|
||||
endpoint_b.connected_endpoint = endpoint_a
|
||||
endpoint_b.connection_status = True
|
||||
endpoint_b.connection_status = path_status
|
||||
endpoint_b.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Cable)
|
||||
def nullify_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
When a Cable is deleted, check for and update its two connected endpoints
|
||||
"""
|
||||
endpoint_a, endpoint_b, _ = instance.get_path_endpoints()
|
||||
|
||||
# Disassociate the Cable from its termination points
|
||||
instance.termination_a.cable = None
|
||||
instance.termination_a.save()
|
||||
instance.termination_b.cable = None
|
||||
instance.termination_b.save()
|
||||
if instance.termination_a is not None:
|
||||
instance.termination_a.cable = None
|
||||
instance.termination_a.save()
|
||||
if instance.termination_b is not None:
|
||||
instance.termination_b.cable = None
|
||||
instance.termination_b.save()
|
||||
|
||||
# If this Cable was part of a complete path, tear it down
|
||||
endpoint_a, endpoint_b = instance.get_path_endpoints()
|
||||
if endpoint_a is not None and endpoint_b is not None:
|
||||
endpoint_a.connected_endpoint = None
|
||||
endpoint_a.connection_status = None
|
||||
|
||||
@@ -4,10 +4,11 @@ from django_tables2.utils import Accessor
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
@@ -260,7 +261,7 @@ class RackRoleTable(BaseTable):
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
model = RackRole
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
|
||||
|
||||
|
||||
@@ -593,7 +594,7 @@ class FrontPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('name', 'type', 'rear_port', 'rear_port_position')
|
||||
fields = ('name', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@@ -601,7 +602,7 @@ class RearPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('name', 'type', 'positions')
|
||||
fields = ('name', 'type', 'positions', 'description')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@@ -668,19 +669,22 @@ class ConsoleConnectionTable(BaseTable):
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('connected_endpoint.device'),
|
||||
args=[Accessor('connected_endpoint.device.pk')],
|
||||
verbose_name='Console server'
|
||||
verbose_name='Console Server'
|
||||
)
|
||||
connected_endpoint = tables.Column(verbose_name='Port')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
cable = tables.LinkColumn(
|
||||
viewname='dcim:cable',
|
||||
args=[Accessor('cable.pk')]
|
||||
connected_endpoint = tables.Column(
|
||||
verbose_name='Port'
|
||||
)
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
name = tables.Column(
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'cable')
|
||||
model = ConsoleConnection
|
||||
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status')
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
@@ -690,17 +694,20 @@ class PowerConnectionTable(BaseTable):
|
||||
args=[Accessor('connected_endpoint.device.pk')],
|
||||
verbose_name='PDU'
|
||||
)
|
||||
connected_endpoint = tables.Column(verbose_name='Outlet')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Power Port')
|
||||
cable = tables.LinkColumn(
|
||||
viewname='dcim:cable',
|
||||
args=[Accessor('cable.pk')]
|
||||
connected_endpoint = tables.Column(
|
||||
verbose_name='Outlet'
|
||||
)
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
name = tables.Column(
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('pdu', 'connected_endpoint', 'device', 'name', 'cable')
|
||||
model = PowerConnection
|
||||
fields = ('pdu', 'connected_endpoint', 'device', 'name', 'connection_status')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
@@ -736,14 +743,12 @@ class InterfaceConnectionTable(BaseTable):
|
||||
accessor=Accessor('connected_endpoint.description'),
|
||||
verbose_name='Description'
|
||||
)
|
||||
cable = tables.LinkColumn(
|
||||
viewname='dcim:cable',
|
||||
args=[Accessor('cable.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'cable')
|
||||
model = InterfaceConnection
|
||||
fields = (
|
||||
'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -3433,7 +3433,7 @@ class VirtualChassisTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'url']
|
||||
['id', 'master', 'url']
|
||||
)
|
||||
|
||||
def test_create_virtualchassis(self):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
@@ -149,3 +150,198 @@ class RackTestCase(TestCase):
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
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'
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='TestDevice1', site=site
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
||||
self.cable.save()
|
||||
|
||||
self.power_port1 = PowerPort.objects.create(device=self.device2, name='psu1')
|
||||
self.patch_pannel = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site
|
||||
)
|
||||
self.rear_port = RearPort.objects.create(device=self.patch_pannel, name='R1', type=1000)
|
||||
self.front_port = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port
|
||||
)
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
When a new Cable is created, it must be cached on either termination point.
|
||||
"""
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(self.cable.termination_a, interface1)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
self.assertEqual(self.cable.termination_b, interface2)
|
||||
|
||||
def test_cable_deletion(self):
|
||||
"""
|
||||
When a Cable is deleted, the `cable` field on its termination points must be nullified.
|
||||
"""
|
||||
self.cable.delete()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.cable)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
self.assertIsNone(interface2.cable)
|
||||
|
||||
def test_cabletermination_deletion(self):
|
||||
"""
|
||||
When a CableTermination object is deleted, its attached Cable (if any) must also be deleted.
|
||||
"""
|
||||
self.interface1.delete()
|
||||
cable = Cable.objects.filter(pk=self.cable.pk).first()
|
||||
self.assertIsNone(cable)
|
||||
|
||||
def test_cable_validates_compatibale_types(self):
|
||||
"""
|
||||
The clean method should have a check to ensure only compatiable port types can be connected by a cable
|
||||
"""
|
||||
# An interface cannot be connected to a power port
|
||||
cable = Cable(termination_a=self.interface1, termination_b=self.power_port1)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_have_the_same_terminination_on_both_ends(self):
|
||||
"""
|
||||
A cable cannot be made with the same A and B side terminations
|
||||
"""
|
||||
cable = Cable(termination_a=self.interface1, termination_b=self.interface1)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_front_port_cannot_connect_to_corresponding_rear_port(self):
|
||||
"""
|
||||
A cable cannot connect a front port to its sorresponding rear port
|
||||
"""
|
||||
cable = Cable(termination_a=self.front_port, termination_b=self.rear_port)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_be_connected_to_an_existing_connection(self):
|
||||
"""
|
||||
Either side of a cable cannot be terminated when that side aready has a connection
|
||||
"""
|
||||
# Try to create a cable with the same interface terminations
|
||||
cable = Cable(termination_a=self.interface2, termination_b=self.interface1)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_connect_to_a_virtual_inteface(self):
|
||||
"""
|
||||
A cable connection cannot include a virtual interface
|
||||
"""
|
||||
virtual_interface = Interface(device=self.device1, name="V1", form_factor=0)
|
||||
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
|
||||
class CablePathTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
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'
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.panel1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=site
|
||||
)
|
||||
self.panel2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
|
||||
)
|
||||
self.rear_port1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
|
||||
)
|
||||
self.front_port1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
|
||||
)
|
||||
self.rear_port2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
|
||||
)
|
||||
self.front_port2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
|
||||
)
|
||||
|
||||
def test_path_completion(self):
|
||||
|
||||
# First segment
|
||||
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||
cable1.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
|
||||
# Second segment
|
||||
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||
cable2.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
|
||||
# Third segment
|
||||
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED)
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
|
||||
|
||||
# Switch third segment from planned to connected
|
||||
cable3.status = CONNECTION_STATUS_CONNECTED
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
def test_path_teardown(self):
|
||||
|
||||
# Build the path
|
||||
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||
cable1.save()
|
||||
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||
cable2.save()
|
||||
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2)
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
# Remove a cable
|
||||
cable2.delete()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
self.assertIsNone(interface2.connected_endpoint)
|
||||
self.assertIsNone(interface2.connection_status)
|
||||
|
||||
@@ -169,13 +169,13 @@ urlpatterns = [
|
||||
# Console server ports
|
||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
url(r'^console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
@@ -189,19 +189,18 @@ urlpatterns = [
|
||||
# Power outlets
|
||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
url(r'^power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
|
||||
@@ -211,6 +210,7 @@ urlpatterns = [
|
||||
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
url(r'^interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
|
||||
# Front ports
|
||||
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
@@ -221,6 +221,7 @@ urlpatterns = [
|
||||
url(r'^front-ports/(?P<pk>\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
url(r'^front-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
url(r'^front-ports/rename/$', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
url(r'^front-ports/disconnect/$', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
|
||||
# Rear ports
|
||||
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
@@ -231,6 +232,7 @@ urlpatterns = [
|
||||
url(r'^rear-ports/(?P<pk>\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
url(r'^rear-ports/rename/$', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
url(r'^rear-ports/disconnect/$', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
|
||||
@@ -24,10 +24,11 @@ from utilities.views import (
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@@ -73,7 +74,7 @@ class BulkRenameView(GetReturnURLMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class BulkDisconnectView(View):
|
||||
class BulkDisconnectView(GetReturnURLMixin, View):
|
||||
"""
|
||||
An extendable view for disconnection console/power/interface components in bulk.
|
||||
"""
|
||||
@@ -81,22 +82,30 @@ class BulkDisconnectView(View):
|
||||
form = None
|
||||
template_name = 'dcim/bulk_disconnect.html'
|
||||
|
||||
def disconnect_objects(self, objects):
|
||||
raise NotImplementedError()
|
||||
def post(self, request):
|
||||
|
||||
def post(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
selected_objects = []
|
||||
return_url = self.get_return_url(request)
|
||||
|
||||
if '_confirm' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
count = self.disconnect_objects(form.cleaned_data['pk'])
|
||||
messages.success(request, "Disconnected {} {} on {}".format(
|
||||
count, self.model._meta.verbose_name_plural, device
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
count = 0
|
||||
for obj in self.model.objects.filter(pk__in=form.cleaned_data['pk']):
|
||||
if obj.cable is None:
|
||||
continue
|
||||
obj.cable.delete()
|
||||
count += 1
|
||||
|
||||
messages.success(request, "Disconnected {} {}".format(
|
||||
count, self.model._meta.verbose_name_plural
|
||||
))
|
||||
return redirect(device.get_absolute_url())
|
||||
|
||||
return redirect(return_url)
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||
@@ -104,10 +113,9 @@ class BulkDisconnectView(View):
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'device': device,
|
||||
'obj_type_plural': self.model._meta.verbose_name_plural,
|
||||
'selected_objects': selected_objects,
|
||||
'return_url': device.get_absolute_url(),
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
|
||||
@@ -892,7 +900,7 @@ class DeviceView(View):
|
||||
interfaces = device.vc_interfaces.select_related(
|
||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
|
||||
).prefetch_related(
|
||||
'cable__termination_a', 'cable__termination_b', 'ip_addresses'
|
||||
'cable__termination_a', 'cable__termination_b', 'ip_addresses', 'tags'
|
||||
)
|
||||
|
||||
# Front ports
|
||||
@@ -1138,14 +1146,6 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
|
||||
model = ConsoleServerPort
|
||||
form = forms.ConsoleServerPortBulkDisconnectForm
|
||||
|
||||
def disconnect_objects(self, consoleserverports):
|
||||
return ConsolePort.objects.filter(
|
||||
connected_endpoint__in=consoleserverports
|
||||
).update(
|
||||
connected_endpoint=None,
|
||||
connection_status=None
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
@@ -1222,11 +1222,6 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
|
||||
model = PowerOutlet
|
||||
form = forms.PowerOutletBulkDisconnectForm
|
||||
|
||||
def disconnect_objects(self, poweroutlets):
|
||||
return PowerPort.objects.filter(connected_endpoint__in=poweroutlets).update(
|
||||
connected_endpoint=None, connection_status=None
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
@@ -1302,17 +1297,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = Interface
|
||||
|
||||
|
||||
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model = Interface
|
||||
form = forms.InterfaceBulkDisconnectForm
|
||||
|
||||
def disconnect_objects(self, interfaces):
|
||||
return Interface.objects.filter(_connected_interface__in=interfaces).update(
|
||||
_connected_interface=None, connection_status=None
|
||||
)
|
||||
|
||||
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
queryset = Interface.objects.all()
|
||||
@@ -1327,6 +1311,12 @@ class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
form = forms.InterfaceBulkRenameForm
|
||||
|
||||
|
||||
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model = Interface
|
||||
form = forms.InterfaceBulkDisconnectForm
|
||||
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
queryset = Interface.objects.all()
|
||||
@@ -1365,6 +1355,12 @@ class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
form = forms.FrontPortBulkRenameForm
|
||||
|
||||
|
||||
class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
model = FrontPort
|
||||
form = forms.FrontPortBulkDisconnectForm
|
||||
|
||||
|
||||
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
@@ -1403,6 +1399,12 @@ class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
form = forms.RearPortBulkRenameForm
|
||||
|
||||
|
||||
class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
model = RearPort
|
||||
form = forms.RearPortBulkDisconnectForm
|
||||
|
||||
|
||||
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
@@ -1623,7 +1625,7 @@ class CableTraceView(View):
|
||||
|
||||
return render(request, 'dcim/cable_trace.html', {
|
||||
'obj': obj,
|
||||
'trace': obj.trace(),
|
||||
'trace': obj.trace(follow_circuits=True),
|
||||
})
|
||||
|
||||
|
||||
@@ -1686,7 +1688,7 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class ConsoleConnectionsListView(ObjectListView):
|
||||
queryset = ConsolePort.objects.select_related(
|
||||
queryset = ConsoleConnection.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
@@ -1700,7 +1702,7 @@ class ConsoleConnectionsListView(ObjectListView):
|
||||
|
||||
|
||||
class PowerConnectionsListView(ObjectListView):
|
||||
queryset = PowerPort.objects.select_related(
|
||||
queryset = PowerConnection.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
@@ -1714,7 +1716,7 @@ class PowerConnectionsListView(ObjectListView):
|
||||
|
||||
|
||||
class InterfaceConnectionsListView(ObjectListView):
|
||||
queryset = Interface.objects.select_related(
|
||||
queryset = InterfaceConnection.objects.select_related(
|
||||
'device', 'cable', '_connected_interface__device'
|
||||
).filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
|
||||
23
netbox/extras/api/nested_serializers.py
Normal file
23
netbox/extras/api/nested_serializers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras.models import ReportResult
|
||||
|
||||
__all__ = [
|
||||
'NestedReportResultSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
||||
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='extras-api:report-detail',
|
||||
lookup_field='report',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ReportResult
|
||||
fields = ['url', 'created', 'user', 'failed']
|
||||
@@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.api.serializers import (
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
)
|
||||
@@ -11,12 +11,13 @@ from extras.constants import *
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
)
|
||||
from tenancy.api.serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
)
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
@@ -187,18 +188,6 @@ class ReportResultSerializer(serializers.ModelSerializer):
|
||||
fields = ['created', 'user', 'failed', 'data']
|
||||
|
||||
|
||||
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='extras-api:report-detail',
|
||||
lookup_field='report',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ReportResult
|
||||
fields = ['url', 'created', 'user', 'failed']
|
||||
|
||||
|
||||
class ReportSerializer(serializers.Serializer):
|
||||
module = serializers.CharField(max_length=255)
|
||||
name = serializers.CharField(max_length=255)
|
||||
|
||||
@@ -49,7 +49,7 @@ GRAPH_TYPE_CHOICES = (
|
||||
EXPORTTEMPLATE_MODELS = [
|
||||
'provider', 'circuit', # Circuits
|
||||
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
|
||||
'consoleport', 'powerport', 'interface', 'virtualchassis', # DCIM
|
||||
'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', # DCIM
|
||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||
'secret', # Secrets
|
||||
'tenant', # Tenancy
|
||||
|
||||
@@ -11,8 +11,8 @@ from taggit.models import Tag
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||
JSONField, SlugField,
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
@@ -206,6 +206,11 @@ class AddRemoveTagsForm(forms.Form):
|
||||
self.fields['remove_tags'] = TagField(required=False)
|
||||
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
@@ -225,6 +230,28 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConfigContext.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0
|
||||
)
|
||||
is_active = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False,
|
||||
max_length=100
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 2.0.9 on 2018-11-01 18:39
|
||||
# Generated by Django 2.1.3 on 2018-11-07 20:46
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interface', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
@@ -512,6 +512,7 @@ class TopologyMap(models.Model):
|
||||
).filter(
|
||||
Q(device__in=devices) | Q(_connected_interface__device__in=devices),
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
for interface in connected_interfaces:
|
||||
style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import django_tables2 as tables
|
||||
from taggit.models import Tag
|
||||
from django_tables2.utils import Accessor
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange
|
||||
@@ -13,6 +14,14 @@ TAG_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CONFIGCONTEXT_ACTIONS = """
|
||||
{% if perms.extras.change_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -53,6 +62,10 @@ OBJECTCHANGE_REQUEST_ID = """
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(
|
||||
viewname='extras:tag',
|
||||
args=[Accessor('slug')]
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
@@ -64,6 +77,21 @@ class TagTable(BaseTable):
|
||||
fields = ('pk', 'name', 'items', 'slug', 'actions')
|
||||
|
||||
|
||||
class TaggedItemTable(BaseTable):
|
||||
content_object = tables.TemplateColumn(
|
||||
template_code=TAGGED_ITEM,
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
content_type = tables.Column(
|
||||
verbose_name='Type'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TaggedItem
|
||||
fields = ('content_object', 'content_type')
|
||||
|
||||
|
||||
class ConfigContextTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
||||
|
||||
# Tags
|
||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
@@ -14,6 +15,7 @@ urlpatterns = [
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -7,15 +8,20 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from taggit.models import Tag
|
||||
from django_tables2 import RequestConfig
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters
|
||||
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .forms import (
|
||||
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
|
||||
TagFilterForm, TagForm,
|
||||
)
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
||||
|
||||
|
||||
#
|
||||
@@ -23,11 +29,45 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
#
|
||||
|
||||
class TagListView(ObjectListView):
|
||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
filter = filters.TagFilter
|
||||
filter_form = TagFilterForm
|
||||
table = TagTable
|
||||
template_name = 'extras/tag_list.html'
|
||||
|
||||
|
||||
class TagView(View):
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
tag = get_object_or_404(Tag, slug=slug)
|
||||
tagged_items = TaggedItem.objects.filter(
|
||||
tag=tag
|
||||
).select_related(
|
||||
'content_type'
|
||||
).prefetch_related(
|
||||
'content_object'
|
||||
)
|
||||
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(items_table)
|
||||
|
||||
return render(request, 'extras/tag.html', {
|
||||
'tag': tag,
|
||||
'items_count': tagged_items.count(),
|
||||
'items_table': items_table,
|
||||
})
|
||||
|
||||
|
||||
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'taggit.change_tag'
|
||||
model = Tag
|
||||
@@ -43,7 +83,11 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
@@ -83,6 +127,15 @@ class ConfigContextEditView(ConfigContextCreateView):
|
||||
permission_required = 'extras.change_configcontext'
|
||||
|
||||
|
||||
class ConfigContextBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'extras.change_configcontext'
|
||||
queryset = ConfigContext.objects.all()
|
||||
filter = filters.ConfigContextFilter
|
||||
table = ConfigContextTable
|
||||
form = ConfigContextBulkEditForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'extras.delete_configcontext'
|
||||
model = ConfigContext
|
||||
|
||||
100
netbox/ipam/api/nested_serializers.py
Normal file
100
netbox/ipam/api/nested_serializers.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedAggregateSerializer',
|
||||
'NestedIPAddressSerializer',
|
||||
'NestedPrefixSerializer',
|
||||
'NestedRIRSerializer',
|
||||
'NestedRoleSerializer',
|
||||
'NestedVLANGroupSerializer',
|
||||
'NestedVLANSerializer',
|
||||
'NestedVRFSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class NestedVRFSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['id', 'url', 'name', 'rd']
|
||||
|
||||
|
||||
#
|
||||
# RIRs/aggregates
|
||||
#
|
||||
|
||||
class NestedRIRSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedAggregateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class NestedRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedVLANSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class NestedPrefixSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
|
||||
class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
@@ -5,18 +5,17 @@ from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.constants import (
|
||||
IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
|
||||
)
|
||||
from ipam.constants import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
@@ -35,35 +34,8 @@ class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedVRFSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['id', 'url', 'name', 'rd']
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
|
||||
class RoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'name', 'slug', 'weight']
|
||||
|
||||
|
||||
class NestedRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# RIRs
|
||||
# RIRs/aggregates
|
||||
#
|
||||
|
||||
class RIRSerializer(ValidatedModelSerializer):
|
||||
@@ -73,18 +45,6 @@ class RIRSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'is_private']
|
||||
|
||||
|
||||
class NestedRIRSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
rir = NestedRIRSerializer()
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -98,18 +58,17 @@ class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
read_only_fields = ['family']
|
||||
|
||||
|
||||
class NestedAggregateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||
|
||||
class Meta(AggregateSerializer.Meta):
|
||||
model = Aggregate
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
# VLAN groups
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class RoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'name', 'slug', 'weight']
|
||||
|
||||
|
||||
class VLANGroupSerializer(ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
|
||||
@@ -133,18 +92,6 @@ class VLANGroupSerializer(ValidatedModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||
@@ -176,14 +123,6 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class NestedVLANSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
@@ -206,16 +145,10 @@ class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
read_only_fields = ['family']
|
||||
|
||||
|
||||
class NestedPrefixSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
class AvailablePrefixSerializer(serializers.Serializer):
|
||||
|
||||
"""
|
||||
Representation of a prefix which does not exist in the database.
|
||||
"""
|
||||
def to_representation(self, instance):
|
||||
if self.context.get('vrf'):
|
||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||
@@ -233,11 +166,14 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
#
|
||||
|
||||
class IPAddressInterfaceSerializer(WritableNestedSerializer):
|
||||
"""
|
||||
Nested representation of an Interface which may belong to a Device *or* a VirtualMachine.
|
||||
"""
|
||||
url = serializers.SerializerMethodField() # We're imitating a HyperlinkedIdentityField here
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'device', 'virtual_machine', 'name',
|
||||
@@ -258,6 +194,8 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -269,20 +207,10 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
read_only_fields = ['family']
|
||||
|
||||
|
||||
class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
|
||||
|
||||
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True)
|
||||
|
||||
|
||||
class AvailableIPSerializer(serializers.Serializer):
|
||||
|
||||
"""
|
||||
Representation of an IP address which does not exist in the database.
|
||||
"""
|
||||
def to_representation(self, instance):
|
||||
if self.context.get('vrf'):
|
||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||
|
||||
@@ -96,25 +96,34 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
for i, requested_prefix in enumerate(requested_prefixes):
|
||||
|
||||
# Validate requested prefix size
|
||||
error_msg = None
|
||||
if 'prefix_length' not in requested_prefix:
|
||||
error_msg = "Item {}: prefix_length field missing".format(i)
|
||||
elif not isinstance(requested_prefix['prefix_length'], int):
|
||||
error_msg = "Item {}: Invalid prefix length ({})".format(
|
||||
i, requested_prefix['prefix_length']
|
||||
)
|
||||
elif prefix.family == 4 and requested_prefix['prefix_length'] > 32:
|
||||
error_msg = "Item {}: Invalid prefix length ({}) for IPv4".format(
|
||||
i, requested_prefix['prefix_length']
|
||||
)
|
||||
elif prefix.family == 6 and requested_prefix['prefix_length'] > 128:
|
||||
error_msg = "Item {}: Invalid prefix length ({}) for IPv6".format(
|
||||
i, requested_prefix['prefix_length']
|
||||
)
|
||||
if error_msg:
|
||||
prefix_length = requested_prefix.get('prefix_length')
|
||||
if prefix_length is None:
|
||||
return Response(
|
||||
{
|
||||
"detail": error_msg
|
||||
"detail": "Item {}: prefix_length field missing".format(i)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
try:
|
||||
prefix_length = int(prefix_length)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Item {}: Invalid prefix length ({})".format(i, prefix_length),
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
if prefix.family == 4 and prefix_length > 32:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Item {}: Invalid prefix length ({}) for IPv4".format(i, prefix_length),
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
elif prefix.family == 6 and prefix_length > 128:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Item {}: Invalid prefix length ({}) for IPv6".format(i, prefix_length),
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
@@ -131,7 +140,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
{
|
||||
"detail": "Insufficient space is available to accommodate the requested prefix size(s)"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
|
||||
# Remove the allocated prefix from the list of available prefixes
|
||||
@@ -187,7 +196,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
"detail": "An insufficient number of IP addresses are available within the prefix {} ({} "
|
||||
"requested, {} available)".format(prefix, len(requested_ips), len(available_ips))
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
|
||||
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 NumericInFilter
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from virtualization.models import VirtualMachine
|
||||
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
|
||||
@@ -32,9 +32,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -80,9 +78,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='RIR (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
@@ -184,9 +180,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=PREFIX_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
@@ -316,9 +310,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=IPADDRESS_ROLE_CHOICES
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -438,9 +430,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=VLAN_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
@@ -482,9 +472,7 @@ class ServiceFilter(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Virtual machine (name)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
|
||||
@@ -566,7 +566,7 @@ class PrefixTest(APITestCase):
|
||||
|
||||
# Try to create one more prefix
|
||||
response = self.client.post(url, {'prefix_length': 30}, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
def test_create_multiple_available_prefixes(self):
|
||||
@@ -583,7 +583,7 @@ class PrefixTest(APITestCase):
|
||||
{'prefix_length': 30, 'description': 'Test Prefix 5'},
|
||||
]
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
# Verify that no prefixes were created (the entire /28 is still available)
|
||||
@@ -628,7 +628,7 @@ class PrefixTest(APITestCase):
|
||||
|
||||
# Try to create one more IP
|
||||
response = self.client.post(url, {}, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
def test_create_multiple_available_ips(self):
|
||||
@@ -639,7 +639,7 @@ class PrefixTest(APITestCase):
|
||||
# Try to create nine IPs (only eight are available)
|
||||
data = [{'description': 'Test IP {}'.format(i)} for i in range(1, 10)] # 9 IPs
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
# Verify that no IPs were created (eight are still available)
|
||||
|
||||
@@ -81,10 +81,17 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
|
||||
try:
|
||||
self.count = queryset.count()
|
||||
except (AttributeError, TypeError):
|
||||
if hasattr(queryset, 'all'):
|
||||
# TODO: This breaks filtering by annotated values
|
||||
# Make a clone of the queryset with any annotations stripped (performance hack)
|
||||
qs = queryset.all()
|
||||
qs.query.annotations.clear()
|
||||
self.count = qs.count()
|
||||
|
||||
else:
|
||||
# We're dealing with an iterable, not a QuerySet
|
||||
self.count = len(queryset)
|
||||
|
||||
self.limit = self.get_limit(request)
|
||||
self.offset = self.get_offset(request)
|
||||
self.request = request
|
||||
|
||||
@@ -91,6 +91,10 @@ LOGGING = {}
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = False
|
||||
|
||||
# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to
|
||||
# re-authenticate. (Default: 1209600 [14 days])
|
||||
LOGIN_TIMEOUT = None
|
||||
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
@@ -121,10 +125,6 @@ PAGINATE_COUNT = 50
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = False
|
||||
|
||||
# The Webhook event backend is disabled by default. Set this to True to enable it. Note that this requires a Redis
|
||||
# database be configured and accessible by NetBox (see `REDIS` below).
|
||||
WEBHOOKS_ENABLED = False
|
||||
|
||||
# Redis database settings (optional). A Redis database is required only if the webhooks backend is enabled.
|
||||
REDIS = {
|
||||
'HOST': 'localhost',
|
||||
@@ -138,9 +138,18 @@ REDIS = {
|
||||
# this setting is derived from the installed location.
|
||||
# REPORTS_ROOT = '/opt/netbox/netbox/reports'
|
||||
|
||||
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
|
||||
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
|
||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||
SESSION_FILE_PATH = None
|
||||
|
||||
# Time zone (default: UTC)
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
# The webhooks backend is disabled by default. Set this to True to enable it. Note that this requires a Redis
|
||||
# database be configured and accessible by NetBox.
|
||||
WEBHOOKS_ENABLED = False
|
||||
|
||||
# Date/time formatting. See the following link for supported formats:
|
||||
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
DATE_FORMAT = 'N j, Y'
|
||||
|
||||
@@ -21,7 +21,7 @@ except ImportError:
|
||||
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
|
||||
)
|
||||
|
||||
VERSION = '2.5-beta1'
|
||||
VERSION = '2.5-beta2'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -54,6 +54,7 @@ ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
LOGGING = getattr(configuration, 'LOGGING', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
|
||||
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
|
||||
@@ -65,6 +66,7 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||
REDIS = getattr(configuration, 'REDIS', {})
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
@@ -111,6 +113,17 @@ DATABASES = {
|
||||
'default': configuration.DATABASE,
|
||||
}
|
||||
|
||||
# Sessions
|
||||
if LOGIN_TIMEOUT is not None:
|
||||
if type(LOGIN_TIMEOUT) is not int or LOGIN_TIMEOUT < 0:
|
||||
raise ImproperlyConfigured(
|
||||
"LOGIN_TIMEOUT must be a positive integer (value: {})".format(LOGIN_TIMEOUT)
|
||||
)
|
||||
# Django default is 1209600 seconds (14 days)
|
||||
SESSION_COOKIE_AGE = LOGIN_TIMEOUT
|
||||
if SESSION_FILE_PATH is not None:
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
|
||||
|
||||
# Redis
|
||||
REDIS_HOST = REDIS.get('HOST', 'localhost')
|
||||
REDIS_PORT = REDIS.get('PORT', 6379)
|
||||
|
||||
@@ -14,7 +14,7 @@ from dcim.filters import (
|
||||
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
|
||||
)
|
||||
from dcim.models import (
|
||||
ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis
|
||||
Cable, ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis
|
||||
)
|
||||
from dcim.tables import (
|
||||
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
|
||||
@@ -166,6 +166,7 @@ class HomeView(View):
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
cables = Cable.objects.all()
|
||||
|
||||
stats = {
|
||||
|
||||
@@ -177,6 +178,7 @@ class HomeView(View):
|
||||
'rack_count': Rack.objects.count(),
|
||||
'device_count': Device.objects.count(),
|
||||
'interface_connections_count': connected_interfaces.count(),
|
||||
'cable_count': cables.count(),
|
||||
'console_connections_count': connected_consoleports.count(),
|
||||
'power_connections_count': connected_powerports.count(),
|
||||
|
||||
@@ -205,7 +207,7 @@ class HomeView(View):
|
||||
'stats': stats,
|
||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||
'report_results': ReportResult.objects.order_by('-created')[:10],
|
||||
'changelog': ObjectChange.objects.select_related('user')[:50]
|
||||
'changelog': ObjectChange.objects.select_related('user', 'changed_object_type')[:50]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -390,6 +390,19 @@ table.report th a {
|
||||
top: -51px;
|
||||
}
|
||||
|
||||
/* Rendered Markdown */
|
||||
.rendered-markdown table {
|
||||
width: 100%;
|
||||
}
|
||||
.rendered-markdown th {
|
||||
border-bottom: 2px solid #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
.rendered-markdown td {
|
||||
border-top: 1px solid #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
.loading {
|
||||
position: fixed;
|
||||
|
||||
@@ -91,8 +91,9 @@ $(document).ready(function() {
|
||||
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||
var match;
|
||||
var rendered_url = api_url;
|
||||
var filter_field;
|
||||
while (match = filter_regex.exec(api_url)) {
|
||||
var filter_field = $('#id_' + match[1]);
|
||||
filter_field = $('#id_' + match[1]);
|
||||
var custom_attr = $('option:selected', filter_field).attr('api-value');
|
||||
if (custom_attr) {
|
||||
rendered_url = rendered_url.replace(match[0], custom_attr);
|
||||
@@ -103,6 +104,18 @@ $(document).ready(function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Account for any conditional URL append strings
|
||||
$.each(child_field[0].attributes, function(index, attr){
|
||||
if (attr.name.includes("data-url-conditional-append-")){
|
||||
var conditional = attr.name.split("data-url-conditional-append-")[1].split("__");
|
||||
var field = $("#id_" + conditional[0]);
|
||||
var field_value = conditional[1];
|
||||
if ($('option:selected', field).attr('api-value') === field_value){
|
||||
rendered_url = rendered_url + attr.value;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If all URL variables have been replaced, make the API call
|
||||
if (rendered_url.search('{{') < 0) {
|
||||
console.log(child_name + ": Fetching " + rendered_url);
|
||||
|
||||
@@ -42,8 +42,8 @@ $(document).ready(function() {
|
||||
event.preventDefault();
|
||||
search_field.val(ui.item.label);
|
||||
select_fields.val('');
|
||||
select_fields.attr('disabled', 'disabled');
|
||||
real_field.empty();
|
||||
select_fields.attr('disabled', 'disabled');
|
||||
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
|
||||
real_field.change();
|
||||
// Disable parent selection fields
|
||||
|
||||
16
netbox/secrets/api/nested_serializers.py
Normal file
16
netbox/secrets/api/nested_serializers.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from secrets.models import SecretRole
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedSecretRoleSerializer'
|
||||
]
|
||||
|
||||
|
||||
class NestedSecretRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
@@ -2,14 +2,15 @@ from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from secrets.models import Secret, SecretRole
|
||||
from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
# SecretRoles
|
||||
# Secrets
|
||||
#
|
||||
|
||||
class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
@@ -19,18 +20,6 @@ class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedSecretRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Secrets
|
||||
#
|
||||
|
||||
class SecretSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
role = NestedSecretRoleSerializer()
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NumericInFilter
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from .models import Secret, SecretRole
|
||||
|
||||
|
||||
@@ -43,9 +43,7 @@ class SecretFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
@@ -386,6 +387,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
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.
|
||||
if len(s) <= 62:
|
||||
pad_length = 62 - len(s)
|
||||
@@ -393,12 +395,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
pad_length = 16 - ((len(s) + 2) % 16)
|
||||
else:
|
||||
pad_length = 0
|
||||
return (
|
||||
chr(len(s) >> 8).encode() +
|
||||
chr(len(s) % 256).encode() +
|
||||
s +
|
||||
os.urandom(pad_length)
|
||||
)
|
||||
|
||||
# Python 2 compatibility
|
||||
if sys.version_info[0] < 3:
|
||||
header = chr(len(s) >> 8) + chr(len(s) % 256)
|
||||
else:
|
||||
header = bytes([len(s) >> 8]) + bytes([len(s) % 256])
|
||||
|
||||
return header + s + os.urandom(pad_length)
|
||||
|
||||
def _unpad(self, s):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import string
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
@@ -86,7 +88,7 @@ class SecretTestCase(TestCase):
|
||||
"""
|
||||
Test basic encryption and decryption functionality using a random master key.
|
||||
"""
|
||||
plaintext = "FooBar123"
|
||||
plaintext = string.printable * 2
|
||||
secret_key = generate_random_key()
|
||||
s = Secret(plaintext=plaintext)
|
||||
s.encrypt(secret_key)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if circuit.comments %}
|
||||
{{ circuit.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -40,13 +40,20 @@
|
||||
<td>Termination</td>
|
||||
<td>
|
||||
{% if termination.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i> Disconnect
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
|
||||
{% if termination.connected_endpoint %}
|
||||
to <a href="{% url 'dcim:device' pk=termination.connected_endpoint.device.pk %}">{{ termination.connected_endpoint.device }}</a>
|
||||
<i class="fa fa-angle-right"></i> {{ termination.connected_endpoint }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if perms.circuits.change_circuittermination %}
|
||||
{% if perms.circuits.add_cable %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk %}?return_url={{ circuit.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i> Connect
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if provider.comments %}
|
||||
{{ provider.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on <strong>{{ device }}</strong>?</p>
|
||||
<p>Are you sure you want to disconnect these {{ selected_objects|length }} {{ obj_type_plural }}?</p>
|
||||
<ul>
|
||||
{% for obj in selected_objects %}
|
||||
<li>{{ obj }}</li>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
@@ -49,6 +50,12 @@
|
||||
<p class="form-control-static">{{ termination_a.device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Type</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ termination_a|model_name|capfirst }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Name</label>
|
||||
<div class="col-md-9">
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_cable %}
|
||||
{% import_button 'dcim:cable_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Cables{% endblock %}</h1>
|
||||
|
||||
@@ -23,18 +23,24 @@
|
||||
{% include 'dcim/inc/cable_trace_end.html' with end=near_end %}
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
<h4>
|
||||
<a href="{% url 'dcim:cable' pk=cable.pk %}">
|
||||
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
||||
</a>
|
||||
</h4>
|
||||
{{ cable.get_status_display }}<br />
|
||||
{{ cable.get_type_display|default:"" }}
|
||||
{% if cable.length %}- {{ cable.length }}{{ cable.length_unit }}{% endif %}
|
||||
<span class="label color-block center-block" style="background-color: #{{ cable.color }}"> </span>
|
||||
{% if cable %}
|
||||
<h4>
|
||||
<a href="{% url 'dcim:cable' pk=cable.pk %}">
|
||||
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
||||
</a>
|
||||
</h4>
|
||||
{{ cable.get_status_display }}<br />
|
||||
{{ cable.get_type_display|default:"" }}
|
||||
{% if cable.length %}- {{ cable.length }}{{ cable.length_unit }}{% endif %}
|
||||
<span class="label color-block center-block" style="background-color: #{{ cable.color }}"> </span>
|
||||
{% else %}
|
||||
<h4 class="text-muted">No Cable</h4>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'dcim/inc/cable_trace_end.html' with end=far_end %}
|
||||
{% if far_end %}
|
||||
{% include 'dcim/inc/cable_trace_end.html' with end=far_end %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if not forloop.last %}<hr />{% endif %}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -293,7 +293,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if device.comments %}
|
||||
{{ device.comments|gfm }}
|
||||
{% else %}
|
||||
@@ -508,6 +508,7 @@
|
||||
<th>Name</th>
|
||||
<th>LAG</th>
|
||||
<th>Description</th>
|
||||
<th>MTU</th>
|
||||
<th>Mode</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
@@ -530,7 +531,7 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -585,7 +586,7 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -640,7 +641,7 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -680,6 +681,7 @@
|
||||
<th>Type</th>
|
||||
<th>Rear Port</th>
|
||||
<th>Position</th>
|
||||
<th>Description</th>
|
||||
<th>Connected Cable</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -695,6 +697,9 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if front_ports and perms.dcim.delete_frontport %}
|
||||
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
@@ -729,6 +734,7 @@
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Positions</th>
|
||||
<th>Description</th>
|
||||
<th>Connected Cable</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -744,6 +750,9 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if rear_ports and perms.dcim.delete_rearport %}
|
||||
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
@@ -770,8 +779,8 @@
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
function toggleConnection(elem, api_url) {
|
||||
var url = netbox_api_path + api_url + elem.attr('data') + "/";
|
||||
function toggleConnection(elem) {
|
||||
var url = netbox_api_path + "dcim/cables/" + elem.attr('data') + "/";
|
||||
if (elem.hasClass('connected')) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
@@ -781,7 +790,7 @@ function toggleConnection(elem, api_url) {
|
||||
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
|
||||
},
|
||||
data: {
|
||||
'connection_status': 'False'
|
||||
'status': 'False'
|
||||
},
|
||||
context: this,
|
||||
success: function() {
|
||||
@@ -800,7 +809,7 @@ function toggleConnection(elem, api_url) {
|
||||
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
|
||||
},
|
||||
data: {
|
||||
'connection_status': 'True'
|
||||
'status': 'True'
|
||||
},
|
||||
context: this,
|
||||
success: function() {
|
||||
@@ -813,14 +822,8 @@ function toggleConnection(elem, api_url) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$(".consoleport-toggle").click(function() {
|
||||
return toggleConnection($(this), "dcim/console-ports/");
|
||||
});
|
||||
$(".powerport-toggle").click(function() {
|
||||
return toggleConnection($(this), "dcim/power-ports/");
|
||||
});
|
||||
$(".interface-toggle").click(function() {
|
||||
return toggleConnection($(this), "dcim/interface-connections/");
|
||||
$(".cable-toggle").click(function() {
|
||||
return toggleConnection($(this));
|
||||
});
|
||||
// Toggle the display of IP addresses under interfaces
|
||||
$('button.toggle-ips').click(function() {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -116,7 +116,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if devicetype.comments %}
|
||||
{{ devicetype.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load helpers %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if termination.device %}
|
||||
{# Device component #}
|
||||
@@ -7,6 +8,12 @@
|
||||
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
{{ termination|model_name|capfirst }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Component</td>
|
||||
<td>{{ termination }}</td>
|
||||
|
||||
16
netbox/templates/dcim/inc/cable_toggle_buttons.html
Normal file
16
netbox/templates/dcim/inc/cable_toggle_buttons.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% if perms.dcim.change_cable %}
|
||||
{% if cable.status %}
|
||||
<a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs cable-toggle" title="Mark installed" data="{{ cable.pk }}">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=cable.pk %}?return_url={{ device.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="consoleport{% if cp.connected_endpoint %} {% if cp.connection_status %}success{% else %}info{% endif %}{% endif %}">
|
||||
<tr class="consoleport{% if cp.cable.status %} success{% elif cp.cable %} info{% endif %}">
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
@@ -30,25 +30,14 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if cp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=cp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
{% if cp.connected_endpoint %}
|
||||
{% if cp.connection_status %}
|
||||
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ cp.pk }}">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ cp.pk }}">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=cp.cable.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleport_connect' termination_a_id=cp.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="consoleserverport{% if csp.connected_endpoint %} {%if csp.connected_endpoint.connection_status %}success{% else %}info{% endif %}{% endif %}">
|
||||
<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
@@ -37,25 +37,14 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if csp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=csp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
{% if csp.connected_endpoint %}
|
||||
{% if csp.connected_endpoint.connection_status %}
|
||||
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ csp.connected_endpoint.pk }}">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ csp.connected_endpoint.pk }}">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=csp.cable.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleserverport_connect' termination_a_id=csp.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tr class="frontport{% if frontport.cable %} {% if frontport.cable.status %}success{% else %}info{% endif %}{% endif %}">
|
||||
{% load helpers %}
|
||||
<tr class="frontport{% if frontport.cable.status %} success{% elif frontport.cable %} info{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
|
||||
@@ -19,6 +20,9 @@
|
||||
<td>{{ frontport.rear_port }}</td>
|
||||
<td>{{ frontport.rear_port_position }}</td>
|
||||
|
||||
{# Description #}
|
||||
<td>{{ frontport.description|placeholder }}</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if frontport.cable %}
|
||||
@@ -30,7 +34,9 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.add_cable %}
|
||||
{% if frontport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=frontport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:frontport_connect' termination_a_id=frontport.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connected_endpoint %} {% if iface.connection_status %}success{% else %}info{% endif %}{% elif iface.circuit_termination %} success{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||
{% load helpers %}
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
@@ -13,17 +14,32 @@
|
||||
<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>
|
||||
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
||||
</span>
|
||||
{% if iface.mac_address %}
|
||||
<br/><small class="text-muted">{{ iface.mac_address }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# LAG #}
|
||||
<td>
|
||||
{% if iface.lag %}
|
||||
<a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
|
||||
<a href="#interface_{{ iface.lag }}" class="label label-primary" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>{{ iface.description|default:"—" }}</td>
|
||||
{# Description/tags #}
|
||||
<td>
|
||||
{% if iface.description %}
|
||||
{{ iface.description }}<br/>
|
||||
{% endif %}
|
||||
{% for tag in iface.tags.all %}
|
||||
{% tag tag %}
|
||||
{% empty %}
|
||||
{% if not iface.description %}—{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
{# MTU #}
|
||||
<td>{{ iface.mtu|default:"—" }}</td>
|
||||
|
||||
{# 802.1Q mode #}
|
||||
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||
@@ -44,7 +60,13 @@
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">
|
||||
LAG interface<br />
|
||||
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
<small class="text-muted">
|
||||
{% for member in iface.member_interfaces.all %}
|
||||
<a href="#interface_{{ member.name }}">{{ member }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% empty %}
|
||||
No members
|
||||
{% endfor %}
|
||||
</small>
|
||||
</td>
|
||||
{% elif iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
@@ -104,25 +126,12 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if not iface.is_virtual %}
|
||||
{% if iface.cable %}
|
||||
{% if iface.cable.status %}
|
||||
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.cable.pk }}" title="Mark planned">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.cable.pk }}" title="Mark installed">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=iface.cable.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Remove cable">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if iface.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=iface.cable %}
|
||||
{% elif not iface.is_virtual and perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
@@ -151,7 +160,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# IP addresses table #}
|
||||
<td colspan="8" style="padding: 0">
|
||||
<td colspan="9" style="padding: 0">
|
||||
<table class="table table-condensed interface-ips">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="poweroutlet{% if po.connected_endpoint %} {% if po.connected_endpoint.connection_status %}success{% else %}info{% endif %}{% endif %}">
|
||||
<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
@@ -37,25 +37,14 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if po.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=po.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=po.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_poweroutlet %}
|
||||
{% if po.connected_endpoint %}
|
||||
{% if po.connected_endpoint.connection_status %}
|
||||
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ po.connected_endpoint.pk }}">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ po.connected_endpoint.pk }}">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=po.cable.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=po.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="powerport{% if pp.connected_endpoint %} {% if pp.connection_status %}success{% else %}info{% endif %}{% endif %}">
|
||||
<tr class="powerport{% if pp.cable.status %} success{% elif pp.cable %} info{% endif %}">
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
@@ -30,25 +30,14 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if pp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=pp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:powerport_connect' termination_a_id=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_powerport %}
|
||||
{% if pp.connected_endpoint %}
|
||||
{% if pp.connection_status %}
|
||||
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ pp.pk }}">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ pp.pk }}">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=pp.cable.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:powerport_connect' termination_a_id=pp.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tr class="rearport{% if rearport.cable %} {% if rearport.cable.status %}success{% else %}info{% endif %}{% endif %}">
|
||||
{% load helpers %}
|
||||
<tr class="rearport{% if rearport.cable.status %} success{% elif rearport.cable %} info{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
|
||||
@@ -18,6 +19,9 @@
|
||||
{# Positions #}
|
||||
<td>{{ rearport.positions }}</td>
|
||||
|
||||
{# Description #}
|
||||
<td>{{ rearport.description|placeholder }}</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if rearport.cable %}
|
||||
@@ -29,7 +33,9 @@
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.add_cable %}
|
||||
{% if rearport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=rearport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:rearport_connect' termination_a_id=rearport.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if rack.comments %}
|
||||
{{ rack.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if site.comments %}
|
||||
{{ site.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
|
||||
69
netbox/templates/extras/tag.html
Normal file
69
netbox/templates/extras/tag.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'extras:tag_list' %}">Tags</a></li>
|
||||
<li>{{ tag }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'extras:tag_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.taggit.change_tag %}
|
||||
<a href="{% url 'extras:tag_edit' slug=tag.slug %}?return_url={% url 'extras:tag' slug=tag.slug %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this tag
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Tag</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ tag.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>
|
||||
{{ tag.slug }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tagged Items</td>
|
||||
<td>
|
||||
{{ items_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,11 @@
|
||||
{% block content %}
|
||||
<h1>{% block title %}Tags{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Connections</h4>
|
||||
<span class="badge pull-right">{{ stats.cable_count }}</span>
|
||||
<p style="padding-left: 20px;"><a href="{% url 'dcim:cable_list' %}">Cables</a></p>
|
||||
<span class="badge pull-right">{{ stats.interface_connections_count }}</span>
|
||||
<p style="padding-left: 20px;"><a href="{% url 'dcim:interface_connections_list' %}">Interfaces</a></p>
|
||||
<span class="badge pull-right">{{ stats.console_connections_count }}</span>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if tenant.comments %}
|
||||
{{ tenant.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if cluster.comments %}
|
||||
{{ cluster.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if virtualmachine.comments %}
|
||||
{{ virtualmachine.comments|gfm }}
|
||||
{% else %}
|
||||
|
||||
29
netbox/tenancy/api/nested_serializers.py
Normal file
29
netbox/tenancy/api/nested_serializers.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedTenantGroupSerializer',
|
||||
'NestedTenantSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedTenantSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
@@ -1,13 +1,13 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupSerializer(ValidatedModelSerializer):
|
||||
@@ -17,18 +17,6 @@ class TenantGroupSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
group = NestedTenantGroupSerializer(required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -39,11 +27,3 @@ class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class NestedTenantSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NumericInFilter
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
|
||||
18
netbox/users/api/nested_serializers.py
Normal file
18
netbox/users/api/nested_serializers.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
_all_ = [
|
||||
'NestedUserSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Users
|
||||
#
|
||||
|
||||
class NestedUserSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username']
|
||||
@@ -1,10 +1,4 @@
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from utilities.api import WritableNestedSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
class NestedUserSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username']
|
||||
# Placeholder for future serializers
|
||||
|
||||
@@ -71,15 +71,24 @@ class ChoiceField(Field):
|
||||
def to_representation(self, obj):
|
||||
if obj is '':
|
||||
return None
|
||||
return {'value': obj, 'label': self._choices[obj]}
|
||||
data = OrderedDict([
|
||||
('value', obj),
|
||||
('label', self._choices[obj])
|
||||
])
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Hotwiring boolean values
|
||||
if hasattr(data, 'lower'):
|
||||
# Hotwiring boolean values from string
|
||||
if data.lower() == 'true':
|
||||
return True
|
||||
if data.lower() == 'false':
|
||||
return False
|
||||
# Check for string representation of an integer (e.g. "123")
|
||||
try:
|
||||
data = int(data)
|
||||
except ValueError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
from taggit.models import Tag
|
||||
|
||||
|
||||
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
@@ -19,3 +20,18 @@ class NullableCharFieldFilter(django_filters.CharFilter):
|
||||
return super(NullableCharFieldFilter, self).filter(qs, value)
|
||||
qs = self.get_method(qs)(**{'{}__isnull'.format(self.name): True})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
"""
|
||||
Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered
|
||||
to objects matching all tags.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
kwargs.setdefault('field_name', 'tags__slug')
|
||||
kwargs.setdefault('to_field_name', 'slug')
|
||||
kwargs.setdefault('conjoined', True)
|
||||
kwargs.setdefault('queryset', Tag.objects.all())
|
||||
|
||||
super(TagFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -42,6 +42,11 @@ NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
|
||||
ALPHANUMERIC_EXPANSION_PATTERN = r'\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]'
|
||||
IP4_EXPANSION_PATTERN = r'\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
|
||||
IP6_EXPANSION_PATTERN = r'\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
|
||||
BOOLEAN_WITH_BLANK_CHOICES = (
|
||||
('', '---------'),
|
||||
('True', 'Yes'),
|
||||
('False', 'No'),
|
||||
)
|
||||
|
||||
|
||||
def parse_numeric_range(string, base=10):
|
||||
@@ -258,9 +263,21 @@ class APISelect(SelectWithDisabled):
|
||||
:param api_url: API URL
|
||||
:param display_field: (Optional) Field to display for child in selection list. Defaults to `name`.
|
||||
:param disabled_indicator: (Optional) Mark option as disabled if this field equates true.
|
||||
:param url_conditional_append: (Optional) A dict of URL query strings to append to the URL if the
|
||||
condition is met. The condition is the dict key and is specified in the form `<field_name>__<field_value>`.
|
||||
If the provided field value is selected for the given field, the URL query string will be appended to
|
||||
the rendered URL. This is useful in cases where a particular field value dictates an additional API filter.
|
||||
"""
|
||||
|
||||
def __init__(self, api_url, display_field=None, disabled_indicator=None, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
api_url,
|
||||
display_field=None,
|
||||
disabled_indicator=None,
|
||||
url_conditional_append=None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
super(APISelect, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -270,6 +287,9 @@ class APISelect(SelectWithDisabled):
|
||||
self.attrs['display-field'] = display_field
|
||||
if disabled_indicator:
|
||||
self.attrs['disabled-indicator'] = disabled_indicator
|
||||
if url_conditional_append:
|
||||
for key, value in url_conditional_append.items():
|
||||
self.attrs["data-url-conditional-append-{}".format(key)] = value
|
||||
|
||||
|
||||
class APISelectMultiple(APISelect):
|
||||
|
||||
@@ -1 +1 @@
|
||||
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.value %} api-value="{{ widget.label|slugify }}"{% endif %}>{{ widget.label.label|default:widget.label }}</option>
|
||||
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.value %} api-value="{{ widget.label|slugify }}"{% endif %}>{{ widget.label.label|default:widget.label|capfirst }}</option>
|
||||
|
||||
@@ -22,6 +22,7 @@ def oneline(value):
|
||||
"""
|
||||
return value.replace('\n', ' ')
|
||||
|
||||
|
||||
@register.filter()
|
||||
def placeholder(value):
|
||||
"""
|
||||
|
||||
62
netbox/virtualization/api/nested_serializers.py
Normal file
62
netbox/virtualization/api/nested_serializers.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Interface
|
||||
from utilities.api import WritableNestedSerializer
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
__all__ = [
|
||||
'NestedClusterGroupSerializer',
|
||||
'NestedClusterSerializer',
|
||||
'NestedClusterTypeSerializer',
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedVirtualMachineSerializer',
|
||||
]
|
||||
|
||||
#
|
||||
# Clusters
|
||||
#
|
||||
|
||||
|
||||
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
class NestedVirtualMachineSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
|
||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'virtual_machine', 'name']
|
||||
@@ -1,19 +1,21 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||
from dcim.constants import IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
|
||||
from virtualization.constants import VM_STATUS_CHOICES
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
# Cluster types
|
||||
# Clusters
|
||||
#
|
||||
|
||||
class ClusterTypeSerializer(ValidatedModelSerializer):
|
||||
@@ -23,18 +25,6 @@ class ClusterTypeSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Cluster groups
|
||||
#
|
||||
|
||||
class ClusterGroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -42,18 +32,6 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Clusters
|
||||
#
|
||||
|
||||
class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
type = NestedClusterTypeSerializer()
|
||||
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
||||
@@ -67,45 +45,28 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
|
||||
|
||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||
site = NestedSiteSerializer(read_only=True)
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
cluster = NestedClusterSerializer()
|
||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'local_context_data',
|
||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -114,44 +75,27 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
|
||||
class Meta(VirtualMachineSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'local_context_data',
|
||||
'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields',
|
||||
'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
|
||||
class NestedVirtualMachineSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# VM interfaces
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency
|
||||
class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
serializer=InterfaceVLANSerializer,
|
||||
serializer=NestedVLANSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
@@ -163,12 +107,3 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
'id', 'virtual_machine', 'name', 'form_factor', 'enabled', 'mtu', 'mac_address', 'description', 'mode',
|
||||
'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
|
||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'virtual_machine', 'name']
|
||||
|
||||
@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NumericInFilter
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from .constants import VM_STATUS_CHOICES
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
@@ -65,9 +65,7 @@ class ClusterFilter(CustomFieldFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
@@ -172,9 +170,7 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
field_name='tags__slug',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
|
||||
@@ -378,6 +378,18 @@ class VirtualMachineTest(APITestCase):
|
||||
self.assertEqual(virtualmachine4.name, data['name'])
|
||||
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
||||
|
||||
def test_create_virtualmachine_without_cluster(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Virtual Machine 4',
|
||||
}
|
||||
|
||||
url = reverse('virtualization-api:virtualmachine-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(VirtualMachine.objects.count(), 3)
|
||||
|
||||
def test_create_virtualmachine_bulk(self):
|
||||
|
||||
data = [
|
||||
|
||||
@@ -3,7 +3,7 @@ django-cors-headers==2.4.0
|
||||
django-debug-toolbar==1.10.1
|
||||
django-filter==2.0.0
|
||||
django-mptt==0.9.1
|
||||
django-tables2==2.0.2
|
||||
django-tables2==2.0.3
|
||||
django-taggit==0.23.0
|
||||
django-taggit-serializer==0.1.7
|
||||
django-timezone-field==3.0
|
||||
@@ -13,6 +13,6 @@ graphviz==0.10.1
|
||||
Markdown==2.6.11
|
||||
netaddr==0.7.19
|
||||
Pillow==5.3.0
|
||||
psycopg2-binary==2.7.5
|
||||
psycopg2-binary==2.7.6.1
|
||||
py-gfm==0.1.4
|
||||
pycryptodome==3.7.0
|
||||
pycryptodome==3.7.1
|
||||
|
||||
14
scripts/git-hooks/pre-commit
Executable file
14
scripts/git-hooks/pre-commit
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# Create a link to this file at .git/hooks/pre-commit to
|
||||
# force PEP8 validation prior to committing
|
||||
#
|
||||
# Ignored violations:
|
||||
#
|
||||
# W504: Line break after binary operator
|
||||
# E501: Line too long
|
||||
|
||||
exec 1>&2
|
||||
|
||||
echo "Validating PEP8 compliance..."
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
Reference in New Issue
Block a user