mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-10 22:02:17 -06:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
125975832b | ||
|
|
20fed375d1 | ||
|
|
fc1b3d6927 | ||
|
|
aed2a3cd1b | ||
|
|
15babeb584 | ||
|
|
020b5ea870 | ||
|
|
2ee5b2344e | ||
|
|
7616bcad3d | ||
|
|
f76ce980e3 | ||
|
|
9440ac7640 | ||
|
|
0e18997c79 | ||
|
|
95464772ac | ||
|
|
b4445dfdf8 | ||
|
|
fb5dca2711 | ||
|
|
6cdff955dc | ||
|
|
4039753b2f | ||
|
|
9df33cef8b | ||
|
|
e3e9211e8a | ||
|
|
0da113b723 | ||
|
|
e965adad7c | ||
|
|
57b225b680 | ||
|
|
b97597c645 | ||
|
|
162828da90 | ||
|
|
292647da14 | ||
|
|
3a88e43103 | ||
|
|
010765e131 | ||
|
|
bcf22831e2 | ||
|
|
cde6e9757b | ||
|
|
f2d9a3e0a1 | ||
|
|
b917e8d3b0 | ||
|
|
3b26ce6501 | ||
|
|
1b2d3bf08b | ||
|
|
492bc9f86e | ||
|
|
a457a73826 | ||
|
|
ac36339491 | ||
|
|
dbbf7ab664 | ||
|
|
66400a98f1 | ||
|
|
aa50e2e385 | ||
|
|
118b8db209 | ||
|
|
967feb6931 | ||
|
|
e1e41a768a | ||
|
|
c333af33dc | ||
|
|
9e5b482b1d | ||
|
|
771747147c | ||
|
|
bc49979243 | ||
|
|
d46b3e2446 | ||
|
|
2804d89c5e | ||
|
|
fd32a71131 | ||
|
|
1556fd0e92 | ||
|
|
5dce7c4e48 | ||
|
|
4bfc32ec99 | ||
|
|
ff65f7fd7b | ||
|
|
cd2aee3053 | ||
|
|
f224ad2959 | ||
|
|
9d9318f38a | ||
|
|
f43d861b50 | ||
|
|
17714b0c12 | ||
|
|
9914576eaa | ||
|
|
bf8eff11ea | ||
|
|
a6c78b99c4 | ||
|
|
6a56ffc650 | ||
|
|
05059606c5 | ||
|
|
a2ff21fab9 | ||
|
|
134370f48d | ||
|
|
c7fa610842 | ||
|
|
242cb7c7cb | ||
|
|
edb49c7f0a | ||
|
|
3e0a7e7f8a | ||
|
|
cfab9a6a0a | ||
|
|
91b5f6d799 | ||
|
|
d5488ca7da | ||
|
|
f9911bff0d | ||
|
|
d5239191fe | ||
|
|
db7148350e | ||
|
|
c51c20a301 | ||
|
|
f4485dc72a | ||
|
|
f59682a7c9 | ||
|
|
507a023f41 | ||
|
|
ea7386b04b | ||
|
|
81479ac177 | ||
|
|
c7acddbc5c | ||
|
|
1905516536 | ||
|
|
64f34d9cd7 | ||
|
|
98bdb0cb3c | ||
|
|
bba88b2be4 | ||
|
|
12dfd4b6e0 | ||
|
|
209e721219 |
1675
CHANGELOG.md
Normal file
1675
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
15
README.md
15
README.md
@@ -42,3 +42,18 @@ and run `upgrade.sh`.
|
|||||||
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||||
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
||||||
|
|
||||||
|
# Related projects
|
||||||
|
|
||||||
|
## Supported SDK
|
||||||
|
|
||||||
|
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
|
||||||
|
|
||||||
|
## Community SDK
|
||||||
|
|
||||||
|
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
|
||||||
|
|
||||||
|
## Ansible Inventory
|
||||||
|
|
||||||
|
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Contextual Configuration Data
|
# Contextual Configuration Data
|
||||||
|
|
||||||
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
||||||
|
|
||||||
|
Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment.
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener
|
|||||||
|
|
||||||
## Export the Database
|
## Export the Database
|
||||||
|
|
||||||
|
Use the `pg_dump` utility to export the entire database to a file:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
pg_dump netbox > netbox.sql
|
pg_dump netbox > netbox.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||||
|
```
|
||||||
|
|
||||||
## Load an Exported Database
|
## Load an Exported Database
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ While NetBox has many configuration settings, only a few of them must be defined
|
|||||||
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# sudo supervsiorctl restart netbox
|
# sudo supervisorctl restart netbox
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ Device bays represent the ability of a device to house child devices. For exampl
|
|||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
|
|
||||||
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||||
|
|
||||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration.
|
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration.
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ Close the release milestone on GitHub. Ensure that there are no remaining open i
|
|||||||
|
|
||||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||||
|
|
||||||
## Update VERSION
|
## Update Version and Changelog
|
||||||
|
|
||||||
Update the `VERSION` constant in `settings.py` to the new release.
|
Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
|
||||||
|
|
||||||
## Submit a Pull Request
|
## Submit a Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
|||||||
## Option B: Apache
|
## Option B: Apache
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y apache2
|
# apt-get install -y apache2 libapache2-mod-wsgi-py3
|
||||||
```
|
```
|
||||||
|
|
||||||
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
site_name: NetBox
|
site_name: NetBox
|
||||||
|
theme: readthedocs
|
||||||
repo_url: https://github.com/digitalocean/netbox
|
repo_url: https://github.com/digitalocean/netbox
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderViewSet(CustomFieldModelViewSet):
|
class ProviderViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Provider.objects.all()
|
queryset = Provider.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
filter_class = filters.ProviderFilter
|
filter_class = filters.ProviderFilter
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class CircuitTypeViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitViewSet(CustomFieldModelViewSet):
|
class CircuitViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filter_class = filters.CircuitFilter
|
filter_class = filters.CircuitFilter
|
||||||
|
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False)
|
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
|||||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||||
class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
class DeviceIPAddressSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -371,7 +371,7 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||||
class NestedClusterSerializer(serializers.ModelSerializer):
|
class NestedClusterSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -380,7 +380,7 @@ class NestedClusterSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||||
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
master = NestedDeviceSerializer()
|
master = NestedDeviceSerializer()
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False)
|
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
@@ -412,7 +412,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'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',
|
'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',
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated',
|
'last_updated', 'local_context_data',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@@ -448,7 +448,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'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',
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
|
||||||
'config_context', 'created', 'last_updated',
|
'config_context', 'created', 'last_updated', 'local_context_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
@@ -576,7 +576,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||||
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@@ -666,7 +666,7 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# Provide a default value to satisfy UniqueTogetherValidator
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.openapi import Parameter
|
from drf_yasg.openapi import Parameter
|
||||||
@@ -60,7 +60,7 @@ class RegionViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteViewSet(CustomFieldModelViewSet):
|
class SiteViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Site.objects.select_related('region', 'tenant')
|
queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags')
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
filter_class = filters.SiteFilter
|
filter_class = filters.SiteFilter
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class RackRoleViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackViewSet(CustomFieldModelViewSet):
|
class RackViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
filter_class = filters.RackFilter
|
filter_class = filters.RackFilter
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ class ManufacturerViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceType.objects.select_related('manufacturer')
|
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
|
||||||
serializer_class = serializers.DeviceTypeSerializer
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
filter_class = filters.DeviceTypeFilter
|
filter_class = filters.DeviceTypeFilter
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||||
'virtual_chassis__master',
|
'virtual_chassis__master',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||||
)
|
)
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
|
|
||||||
@@ -315,31 +315,31 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortViewSet(ModelViewSet):
|
class ConsolePortViewSet(ModelViewSet):
|
||||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').prefetch_related('tags')
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
filter_class = filters.ConsolePortFilter
|
filter_class = filters.ConsolePortFilter
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortViewSet(ModelViewSet):
|
class ConsoleServerPortViewSet(ModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device').prefetch_related('tags')
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
filter_class = filters.ConsoleServerPortFilter
|
filter_class = filters.ConsoleServerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(ModelViewSet):
|
class PowerPortViewSet(ModelViewSet):
|
||||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').prefetch_related('tags')
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
filter_class = filters.PowerPortFilter
|
filter_class = filters.PowerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(ModelViewSet):
|
class PowerOutletViewSet(ModelViewSet):
|
||||||
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device').prefetch_related('tags')
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
filter_class = filters.PowerOutletFilter
|
filter_class = filters.PowerOutletFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(ModelViewSet):
|
class InterfaceViewSet(ModelViewSet):
|
||||||
queryset = Interface.objects.select_related('device')
|
queryset = Interface.objects.select_related('device').prefetch_related('tags')
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filter_class = filters.InterfaceFilter
|
filter_class = filters.InterfaceFilter
|
||||||
|
|
||||||
@@ -355,13 +355,13 @@ class InterfaceViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBayViewSet(ModelViewSet):
|
class DeviceBayViewSet(ModelViewSet):
|
||||||
queryset = DeviceBay.objects.select_related('installed_device')
|
queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
|
||||||
serializer_class = serializers.DeviceBaySerializer
|
serializer_class = serializers.DeviceBaySerializer
|
||||||
filter_class = filters.DeviceBayFilter
|
filter_class = filters.DeviceBayFilter
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(ModelViewSet):
|
class InventoryItemViewSet(ModelViewSet):
|
||||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags')
|
||||||
serializer_class = serializers.InventoryItemSerializer
|
serializer_class = serializers.InventoryItemSerializer
|
||||||
filter_class = filters.InventoryItemFilter
|
filter_class = filters.InventoryItemFilter
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ class InterfaceConnectionViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisViewSet(ModelViewSet):
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
queryset = VirtualChassis.objects.all()
|
queryset = VirtualChassis.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
|
|||||||
IFACE_FF_FLEXSTACK = 5100
|
IFACE_FF_FLEXSTACK = 5100
|
||||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||||
IFACE_FF_JUNIPER_VCP = 5200
|
IFACE_FF_JUNIPER_VCP = 5200
|
||||||
|
IFACE_FF_SUMMITSTACK = 5300
|
||||||
|
IFACE_FF_SUMMITSTACK128 = 5310
|
||||||
|
IFACE_FF_SUMMITSTACK256 = 5320
|
||||||
|
IFACE_FF_SUMMITSTACK512 = 5330
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
IFACE_FF_OTHER = 32767
|
IFACE_FF_OTHER = 32767
|
||||||
|
|
||||||
@@ -168,6 +173,10 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||||
|
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
|
||||||
|
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
|
||||||
|
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
|
||||||
|
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, mac_unix_expanded
|
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from .formfields import MACAddressFormField
|
|
||||||
|
|
||||||
|
|
||||||
class ASNField(models.BigIntegerField):
|
class ASNField(models.BigIntegerField):
|
||||||
description = "32-bit ASN field"
|
description = "32-bit ASN field"
|
||||||
@@ -35,7 +33,7 @@ class MACAddressField(models.Field):
|
|||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||||
except ValueError as e:
|
except AddrFormatError as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
|
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
@@ -45,11 +43,3 @@ class MACAddressField(models.Field):
|
|||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
return str(self.to_python(value))
|
return str(self.to_python(value))
|
||||||
|
|
||||||
def form_class(self):
|
|
||||||
return MACAddressFormField
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
|
||||||
defaults = {'form_class': self.form_class()}
|
|
||||||
defaults.update(kwargs)
|
|
||||||
return super(MACAddressField, self).formfield(**defaults)
|
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RackGroupFilter(django_filters.FilterSet):
|
class RackGroupFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@@ -127,6 +131,15 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['site_id', 'name', 'slug']
|
fields = ['site_id', 'name', 'slug']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class RackRoleFilter(django_filters.FilterSet):
|
class RackRoleFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from netaddr import EUI, AddrFormatError
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Form fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class MACAddressFormField(forms.Field):
|
|
||||||
default_error_messages = {
|
|
||||||
'invalid': "Enter a valid MAC address.",
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(value, EUI):
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
return EUI(value, version=48)
|
|
||||||
except AddrFormatError:
|
|
||||||
raise ValidationError("Please specify a valid MAC address.")
|
|
||||||
@@ -18,7 +18,7 @@ from utilities.forms import (
|
|||||||
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
|
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
|
||||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
|
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
|
||||||
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
|
FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
from .constants import (
|
from .constants import (
|
||||||
@@ -27,7 +27,6 @@ from .constants import (
|
|||||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||||
SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
|
SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
|
||||||
)
|
)
|
||||||
from .formfields import MACAddressFormField
|
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||||
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||||
@@ -823,16 +822,19 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(required=False)
|
tags = TagField(required=False)
|
||||||
|
local_context_data = JSONField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
|
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
|
||||||
'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
|
'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||||
|
'local_context_data'
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
'serial': "Chassis serial number",
|
'serial': "Chassis serial number",
|
||||||
|
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context"
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'face': forms.Select(attrs={'filter-for': 'position'}),
|
'face': forms.Select(attrs={'filter-for': 'position'}),
|
||||||
@@ -1192,6 +1194,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class ConsolePortCreateForm(ComponentForm):
|
class ConsolePortCreateForm(ComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionCSVForm(forms.ModelForm):
|
class ConsoleConnectionCSVForm(forms.ModelForm):
|
||||||
@@ -1362,6 +1365,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class ConsoleServerPortCreateForm(ComponentForm):
|
class ConsoleServerPortCreateForm(ComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
@@ -1459,6 +1463,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PowerPortCreateForm(ComponentForm):
|
class PowerPortCreateForm(ComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionCSVForm(forms.ModelForm):
|
class PowerConnectionCSVForm(forms.ModelForm):
|
||||||
@@ -1629,6 +1634,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PowerOutletCreateForm(ComponentForm):
|
class PowerOutletCreateForm(ComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
@@ -1795,7 +1801,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
# Compile VLAN choices
|
# Compile VLAN choices
|
||||||
vlan_choices = []
|
vlan_choices = []
|
||||||
|
|
||||||
# Add global VLANs
|
# Add non-grouped global VLANs
|
||||||
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
||||||
vlan_choices.append((
|
vlan_choices.append((
|
||||||
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
||||||
@@ -1808,16 +1814,15 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
||||||
)
|
)
|
||||||
|
|
||||||
parent = self.instance.parent
|
site = getattr(self.instance.parent, 'site', None)
|
||||||
if parent is not None:
|
if site is not None:
|
||||||
|
|
||||||
# Add site VLANs
|
# Add non-grouped site VLANs
|
||||||
if parent.site:
|
site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans)
|
||||||
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
|
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||||
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
|
||||||
|
|
||||||
# Add grouped site VLANs
|
# Add grouped site VLANs
|
||||||
for group in VLANGroup.objects.filter(site=parent.site):
|
for group in VLANGroup.objects.filter(site=site):
|
||||||
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
||||||
vlan_choices.append((
|
vlan_choices.append((
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
'{} / {}'.format(group.site.name, group.name),
|
||||||
@@ -1855,7 +1860,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form):
|
|||||||
enabled = forms.BooleanField(required=False)
|
enabled = forms.BooleanField(required=False)
|
||||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||||
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
|
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
|
||||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
mac_address = forms.CharField(required=False, label='MAC Address')
|
||||||
mgmt_only = forms.BooleanField(
|
mgmt_only = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label='OOB Management',
|
label='OOB Management',
|
||||||
@@ -1863,6 +1868,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form):
|
|||||||
)
|
)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
|
mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@@ -2100,6 +2106,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class DeviceBayCreateForm(ComponentForm):
|
class DeviceBayCreateForm(ComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||||
|
|||||||
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 2.0.8 on 2018-08-22 14:23
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0061_platform_napalm_args'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mtu',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
netbox/dcim/migrations/0063_device_local_context_data.py
Normal file
19
netbox/dcim/migrations/0063_device_local_context_data.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.8 on 2018-09-16 02:01
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0062_interface_mtu'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='local_context_data',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,10 +7,10 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.postgres.fields import ArrayField, JSONField
|
from django.contrib.postgres.fields import ArrayField, JSONField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
@@ -1809,9 +1809,10 @@ class Interface(ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='MAC Address'
|
verbose_name='MAC Address'
|
||||||
)
|
)
|
||||||
mtu = models.PositiveSmallIntegerField(
|
mtu = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||||
verbose_name='MTU'
|
verbose_name='MTU'
|
||||||
)
|
)
|
||||||
mgmt_only = models.BooleanField(
|
mgmt_only = models.BooleanField(
|
||||||
@@ -1933,11 +1934,20 @@ class Interface(ComponentModel):
|
|||||||
"""
|
"""
|
||||||
Include the connected Interface (if any).
|
Include the connected Interface (if any).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
||||||
|
# the component parent will raise DoesNotExist. For more discussion, see
|
||||||
|
# https://github.com/digitalocean/netbox/issues/2323
|
||||||
|
try:
|
||||||
|
parent_obj = self.get_component_parent()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
parent_obj = None
|
||||||
|
|
||||||
ObjectChange(
|
ObjectChange(
|
||||||
user=user,
|
user=user,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
changed_object=self,
|
changed_object=self,
|
||||||
related_object=self.get_component_parent(),
|
related_object=parent_obj,
|
||||||
action=action,
|
action=action,
|
||||||
object_data=serialize_object(self, extra={
|
object_data=serialize_object(self, extra={
|
||||||
'connected_interface': self.connected_interface.pk if self.connection else None,
|
'connected_interface': self.connected_interface.pk if self.connection else None,
|
||||||
@@ -2062,6 +2072,7 @@ class InterfaceConnection(models.Model):
|
|||||||
(self.interface_a, self.interface_b),
|
(self.interface_a, self.interface_b),
|
||||||
(self.interface_b, self.interface_a),
|
(self.interface_b, self.interface_a),
|
||||||
)
|
)
|
||||||
|
|
||||||
for interface, peer_interface in interfaces:
|
for interface, peer_interface in interfaces:
|
||||||
if action == OBJECTCHANGE_ACTION_DELETE:
|
if action == OBJECTCHANGE_ACTION_DELETE:
|
||||||
connection_data = {
|
connection_data = {
|
||||||
@@ -2072,11 +2083,17 @@ class InterfaceConnection(models.Model):
|
|||||||
'connected_interface': peer_interface.pk,
|
'connected_interface': peer_interface.pk,
|
||||||
'connection_status': self.connection_status
|
'connection_status': self.connection_status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent_obj = interface.parent
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
parent_obj = None
|
||||||
|
|
||||||
ObjectChange(
|
ObjectChange(
|
||||||
user=user,
|
user=user,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
changed_object=interface,
|
changed_object=interface,
|
||||||
related_object=interface.parent,
|
related_object=parent_obj,
|
||||||
action=OBJECTCHANGE_ACTION_UPDATE,
|
action=OBJECTCHANGE_ACTION_UPDATE,
|
||||||
object_data=serialize_object(interface, extra=connection_data)
|
object_data=serialize_object(interface, extra=connection_data)
|
||||||
).save()
|
).save()
|
||||||
|
|||||||
@@ -614,10 +614,12 @@ class PowerConnectionTable(BaseTable):
|
|||||||
class InterfaceConnectionTable(BaseTable):
|
class InterfaceConnectionTable(BaseTable):
|
||||||
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'),
|
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'),
|
||||||
args=[Accessor('interface_a.device.pk')], verbose_name='Device A')
|
args=[Accessor('interface_a.device.pk')], verbose_name='Device A')
|
||||||
interface_a = tables.Column(verbose_name='Interface A')
|
interface_a = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_a'),
|
||||||
|
args=[Accessor('interface_a.pk')], verbose_name='Interface A')
|
||||||
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'),
|
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'),
|
||||||
args=[Accessor('interface_b.device.pk')], verbose_name='Device B')
|
args=[Accessor('interface_b.device.pk')], verbose_name='Device B')
|
||||||
interface_b = tables.Column(verbose_name='Interface B')
|
interface_b = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_b'),
|
||||||
|
args=[Accessor('interface_b.pk')], verbose_name='Interface B')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from netaddr import IPNetwork
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.constants import (
|
from dcim.constants import (
|
||||||
@@ -13,9 +14,10 @@ from dcim.models import (
|
|||||||
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
from ipam.models import VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
|
from virtualization.models import Cluster, ClusterType
|
||||||
|
|
||||||
|
|
||||||
class RegionTest(APITestCase):
|
class RegionTest(APITestCase):
|
||||||
@@ -1680,14 +1682,28 @@ class DeviceTest(APITestCase):
|
|||||||
self.devicerole2 = DeviceRole.objects.create(
|
self.devicerole2 = DeviceRole.objects.create(
|
||||||
name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
|
name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
|
||||||
)
|
)
|
||||||
|
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
|
||||||
|
self.cluster1 = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
|
||||||
self.device1 = Device.objects.create(
|
self.device1 = Device.objects.create(
|
||||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 1', site=self.site1
|
device_type=self.devicetype1,
|
||||||
|
device_role=self.devicerole1,
|
||||||
|
name='Test Device 1',
|
||||||
|
site=self.site1,
|
||||||
|
cluster=self.cluster1
|
||||||
)
|
)
|
||||||
self.device2 = Device.objects.create(
|
self.device2 = Device.objects.create(
|
||||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 2', site=self.site1
|
device_type=self.devicetype1,
|
||||||
|
device_role=self.devicerole1,
|
||||||
|
name='Test Device 2',
|
||||||
|
site=self.site1,
|
||||||
|
cluster=self.cluster1
|
||||||
)
|
)
|
||||||
self.device3 = Device.objects.create(
|
self.device3 = Device.objects.create(
|
||||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 3', site=self.site1
|
device_type=self.devicetype1,
|
||||||
|
device_role=self.devicerole1,
|
||||||
|
name='Test Device 3',
|
||||||
|
site=self.site1,
|
||||||
|
cluster=self.cluster1
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_device(self):
|
def test_get_device(self):
|
||||||
@@ -1696,6 +1712,8 @@ class DeviceTest(APITestCase):
|
|||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.data['name'], self.device1.name)
|
self.assertEqual(response.data['name'], self.device1.name)
|
||||||
|
self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
|
||||||
|
self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
|
||||||
|
|
||||||
def test_list_devices(self):
|
def test_list_devices(self):
|
||||||
|
|
||||||
@@ -1711,6 +1729,7 @@ class DeviceTest(APITestCase):
|
|||||||
'device_role': self.devicerole1.pk,
|
'device_role': self.devicerole1.pk,
|
||||||
'name': 'Test Device 4',
|
'name': 'Test Device 4',
|
||||||
'site': self.site1.pk,
|
'site': self.site1.pk,
|
||||||
|
'cluster': self.cluster1.pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('dcim-api:device-list')
|
url = reverse('dcim-api:device-list')
|
||||||
@@ -1722,7 +1741,8 @@ class DeviceTest(APITestCase):
|
|||||||
self.assertEqual(device4.device_type_id, data['device_type'])
|
self.assertEqual(device4.device_type_id, data['device_type'])
|
||||||
self.assertEqual(device4.device_role_id, data['device_role'])
|
self.assertEqual(device4.device_role_id, data['device_role'])
|
||||||
self.assertEqual(device4.name, data['name'])
|
self.assertEqual(device4.name, data['name'])
|
||||||
self.assertEqual(device4.site_id, data['site'])
|
self.assertEqual(device4.site.pk, data['site'])
|
||||||
|
self.assertEqual(device4.cluster.pk, data['cluster'])
|
||||||
|
|
||||||
def test_create_device_bulk(self):
|
def test_create_device_bulk(self):
|
||||||
|
|
||||||
@@ -1758,11 +1778,17 @@ class DeviceTest(APITestCase):
|
|||||||
|
|
||||||
def test_update_device(self):
|
def test_update_device(self):
|
||||||
|
|
||||||
|
interface = Interface.objects.create(name='Test Interface 1', device=self.device1)
|
||||||
|
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
|
||||||
|
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'device_type': self.devicetype2.pk,
|
'device_type': self.devicetype2.pk,
|
||||||
'device_role': self.devicerole2.pk,
|
'device_role': self.devicerole2.pk,
|
||||||
'name': 'Test Device X',
|
'name': 'Test Device X',
|
||||||
'site': self.site2.pk,
|
'site': self.site2.pk,
|
||||||
|
'primary_ip4': ip4_address.pk,
|
||||||
|
'primary_ip6': ip6_address.pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk})
|
url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk})
|
||||||
@@ -1774,7 +1800,9 @@ class DeviceTest(APITestCase):
|
|||||||
self.assertEqual(device1.device_type_id, data['device_type'])
|
self.assertEqual(device1.device_type_id, data['device_type'])
|
||||||
self.assertEqual(device1.device_role_id, data['device_role'])
|
self.assertEqual(device1.device_role_id, data['device_role'])
|
||||||
self.assertEqual(device1.name, data['name'])
|
self.assertEqual(device1.name, data['name'])
|
||||||
self.assertEqual(device1.site_id, data['site'])
|
self.assertEqual(device1.site.pk, data['site'])
|
||||||
|
self.assertEqual(device1.primary_ip4.pk, data['primary_ip4'])
|
||||||
|
self.assertEqual(device1.primary_ip6.pk, data['primary_ip6'])
|
||||||
|
|
||||||
def test_delete_device(self):
|
def test_delete_device(self):
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import LaxURLField
|
||||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, Webhook
|
||||||
from .models import (
|
|
||||||
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
|
|
||||||
Webhook,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def order_content_types(field):
|
def order_content_types(field):
|
||||||
@@ -39,7 +36,7 @@ class WebhookForm(forms.ModelForm):
|
|||||||
order_content_types(self.fields['obj_type'])
|
order_content_types(self.fields['obj_type'])
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Webhook)
|
@admin.register(Webhook, site=admin_site)
|
||||||
class WebhookAdmin(admin.ModelAdmin):
|
class WebhookAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
||||||
@@ -72,7 +69,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
|||||||
extra = 5
|
extra = 5
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomField)
|
@admin.register(CustomField, site=admin_site)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
inlines = [CustomFieldChoiceAdmin]
|
inlines = [CustomFieldChoiceAdmin]
|
||||||
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||||
@@ -86,7 +83,7 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
|||||||
# Graphs
|
# Graphs
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(Graph)
|
@admin.register(Graph, site=admin_site)
|
||||||
class GraphAdmin(admin.ModelAdmin):
|
class GraphAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'type', 'weight', 'source']
|
list_display = ['name', 'type', 'weight', 'source']
|
||||||
|
|
||||||
@@ -109,7 +106,7 @@ class ExportTemplateForm(forms.ModelForm):
|
|||||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ExportTemplate)
|
@admin.register(ExportTemplate, site=admin_site)
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
||||||
form = ExportTemplateForm
|
form = ExportTemplateForm
|
||||||
@@ -119,7 +116,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(TopologyMap)
|
@admin.register(TopologyMap, site=admin_site)
|
||||||
class TopologyMapAdmin(admin.ModelAdmin):
|
class TopologyMapAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'slug', 'site']
|
list_display = ['name', 'slug', 'site']
|
||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
@@ -131,7 +128,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
# User actions
|
# User actions
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(UserAction)
|
@admin.register(UserAction, site=admin_site)
|
||||||
class UserActionAdmin(admin.ModelAdmin):
|
class UserActionAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
||||||
|
|||||||
@@ -138,8 +138,11 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextViewSet(ModelViewSet):
|
class ConfigContextViewSet(ModelViewSet):
|
||||||
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
queryset = ConfigContext.objects.prefetch_related(
|
||||||
|
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||||
|
)
|
||||||
serializer_class = serializers.ConfigContextSerializer
|
serializer_class = serializers.ConfigContextSerializer
|
||||||
|
filter_class = filters.ConfigContextFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
@@ -124,6 +125,92 @@ class TopologyMapFilter(django_filters.FilterSet):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='regions',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region',
|
||||||
|
)
|
||||||
|
region = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='regions__slug',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='sites',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='sites__slug',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site (slug)',
|
||||||
|
)
|
||||||
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='roles',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
label='Role',
|
||||||
|
)
|
||||||
|
role = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='roles__slug',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Role (slug)',
|
||||||
|
)
|
||||||
|
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='platforms',
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
label='Platform',
|
||||||
|
)
|
||||||
|
platform = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='platforms__slug',
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Platform (slug)',
|
||||||
|
)
|
||||||
|
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant_groups',
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
label='Tenant group',
|
||||||
|
)
|
||||||
|
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant_groups__slug',
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant group (slug)',
|
||||||
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenants',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenants__slug',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigContext
|
||||||
|
fields = ['name', 'is_active']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
|
Q(data__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeFilter(django_filters.FilterSet):
|
class ObjectChangeFilter(django_filters.FilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ from mptt.forms import TreeNodeMultipleChoiceField
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Region
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from utilities.forms import (
|
||||||
|
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||||
|
JSONField, SlugField,
|
||||||
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
OBJECTCHANGE_ACTION_CHOICES,
|
OBJECTCHANGE_ACTION_CHOICES,
|
||||||
@@ -223,6 +227,37 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Search'
|
||||||
|
)
|
||||||
|
region = FilterTreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
role = FilterChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
platform = FilterChoiceField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
tenant_group = FilterChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -700,9 +700,22 @@ class ConfigContext(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Verify that JSON data is provided as an object
|
||||||
|
if type(self.data) is not dict:
|
||||||
|
raise ValidationError(
|
||||||
|
{'data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextModel(models.Model):
|
class ConfigContextModel(models.Model):
|
||||||
|
|
||||||
|
local_context_data = JSONField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -716,6 +729,10 @@ class ConfigContextModel(models.Model):
|
|||||||
for context in ConfigContext.objects.get_for_object(self):
|
for context in ConfigContext.objects.get_for_object(self):
|
||||||
data.update(context.data)
|
data.update(context.data)
|
||||||
|
|
||||||
|
# If the object has local config context data defined, that data overwrites all rendered data
|
||||||
|
if self.local_context_data is not None:
|
||||||
|
data.update(self.local_context_data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from collections import OrderedDict
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -16,19 +17,36 @@ def is_report(obj):
|
|||||||
"""
|
"""
|
||||||
Returns True if the given object is a Report.
|
Returns True if the given object is a Report.
|
||||||
"""
|
"""
|
||||||
if obj in Report.__subclasses__():
|
return obj in Report.__subclasses__()
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_report(module_name, report_name):
|
def get_report(module_name, report_name):
|
||||||
"""
|
"""
|
||||||
Return a specific report from within a module.
|
Return a specific report from within a module.
|
||||||
"""
|
"""
|
||||||
module = importlib.import_module('reports.{}'.format(module_name))
|
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||||
|
|
||||||
|
# Python 3.5+
|
||||||
|
if sys.version_info >= (3, 5):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Python 2.7
|
||||||
|
else:
|
||||||
|
import imp
|
||||||
|
try:
|
||||||
|
module = imp.load_source(module_name, file_path)
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
report = getattr(module, report_name, None)
|
report = getattr(module, report_name, None)
|
||||||
if report is None:
|
if report is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return report()
|
return report()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,15 +72,10 @@ class ConfigContextTable(BaseTable):
|
|||||||
is_active = BooleanColumn(
|
is_active = BooleanColumn(
|
||||||
verbose_name='Active'
|
verbose_name='Active'
|
||||||
)
|
)
|
||||||
actions = tables.TemplateColumn(
|
|
||||||
template_code=CONFIGCONTEXT_ACTIONS,
|
|
||||||
attrs={'td': {'class': 'text-right'}},
|
|
||||||
verbose_name=''
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
|
fields = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeTable(BaseTable):
|
class ObjectChangeTable(BaseTable):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from taggit.models import Tag
|
|||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||||
from . import filters
|
from . import filters
|
||||||
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||||
from .reports import get_report, get_reports
|
from .reports import get_report, get_reports
|
||||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||||
@@ -56,6 +56,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
class ConfigContextListView(ObjectListView):
|
class ConfigContextListView(ObjectListView):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
|
filter = filters.ConfigContextFilter
|
||||||
|
filter_form = ConfigContextFilterForm
|
||||||
table = ConfigContextTable
|
table = ConfigContextTable
|
||||||
template_name = 'extras/configcontext_list.html'
|
template_name = 'extras/configcontext_list.html'
|
||||||
|
|
||||||
@@ -104,9 +106,11 @@ class ObjectConfigContextView(View):
|
|||||||
|
|
||||||
obj = get_object_or_404(self.object_class, pk=pk)
|
obj = get_object_or_404(self.object_class, pk=pk)
|
||||||
source_contexts = ConfigContext.objects.get_for_object(obj)
|
source_contexts = ConfigContext.objects.get_for_object(obj)
|
||||||
|
model_name = self.object_class._meta.model_name
|
||||||
|
|
||||||
return render(request, 'extras/object_configcontext.html', {
|
return render(request, 'extras/object_configcontext.html', {
|
||||||
self.object_class._meta.model_name: obj,
|
model_name: obj,
|
||||||
|
'obj': obj,
|
||||||
'rendered_context': obj.get_config_context(),
|
'rendered_context': obj.get_config_context(),
|
||||||
'source_contexts': source_contexts,
|
'source_contexts': source_contexts,
|
||||||
'base_template': self.base_template,
|
'base_template': self.base_template,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import datetime
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from extras.models import Webhook
|
from extras.models import Webhook
|
||||||
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||||
@@ -18,23 +17,16 @@ def enqueue_webhooks(instance, action):
|
|||||||
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
||||||
return
|
return
|
||||||
|
|
||||||
type_create = action == OBJECTCHANGE_ACTION_CREATE
|
# Retrieve any applicable Webhooks
|
||||||
type_update = action == OBJECTCHANGE_ACTION_UPDATE
|
action_flag = {
|
||||||
type_delete = action == OBJECTCHANGE_ACTION_DELETE
|
OBJECTCHANGE_ACTION_CREATE: 'type_create',
|
||||||
|
OBJECTCHANGE_ACTION_UPDATE: 'type_update',
|
||||||
# Find assigned webhooks
|
OBJECTCHANGE_ACTION_DELETE: 'type_delete',
|
||||||
|
}[action]
|
||||||
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
||||||
webhooks = Webhook.objects.filter(
|
webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
|
||||||
Q(enabled=True) &
|
|
||||||
(
|
|
||||||
Q(type_create=type_create) |
|
|
||||||
Q(type_update=type_update) |
|
|
||||||
Q(type_delete=type_delete)
|
|
||||||
) &
|
|
||||||
Q(obj_type=obj_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
if webhooks:
|
if webhooks.exists():
|
||||||
# Get the Model's API serializer class and serialize the object
|
# Get the Model's API serializer class and serialize the object
|
||||||
serializer_class = get_serializer_for_model(instance.__class__)
|
serializer_class = get_serializer_for_model(instance.__class__)
|
||||||
serializer_context = {
|
serializer_context = {
|
||||||
|
|||||||
@@ -37,8 +37,12 @@ def process_webhook(webhook, data, model_class, event, timestamp):
|
|||||||
prepared_request = requests.Request(**params).prepare()
|
prepared_request = requests.Request(**params).prepare()
|
||||||
|
|
||||||
if webhook.secret != '':
|
if webhook.secret != '':
|
||||||
# sign the request with the secret
|
# Sign the request with a hash of the secret key and its content.
|
||||||
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512)
|
hmac_prep = hmac.new(
|
||||||
|
key=webhook.secret.encode('utf8'),
|
||||||
|
msg=prepared_request.body.encode('utf8'),
|
||||||
|
digestmod=hashlib.sha512
|
||||||
|
)
|
||||||
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
||||||
|
|
||||||
with requests.Session() as session:
|
with requests.Session() as session:
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False)
|
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
|
||||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VRFViewSet(CustomFieldModelViewSet):
|
class VRFViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VRF.objects.select_related('tenant')
|
queryset = VRF.objects.select_related('tenant').prefetch_related('tags')
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
filter_class = filters.VRFFilter
|
filter_class = filters.VRFFilter
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class RIRViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class AggregateViewSet(CustomFieldModelViewSet):
|
class AggregateViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Aggregate.objects.select_related('rir')
|
queryset = Aggregate.objects.select_related('rir').prefetch_related('tags')
|
||||||
serializer_class = serializers.AggregateSerializer
|
serializer_class = serializers.AggregateSerializer
|
||||||
filter_class = filters.AggregateFilter
|
filter_class = filters.AggregateFilter
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class RoleViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixViewSet(CustomFieldModelViewSet):
|
class PrefixViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').prefetch_related('tags')
|
||||||
serializer_class = serializers.PrefixSerializer
|
serializer_class = serializers.PrefixSerializer
|
||||||
filter_class = filters.PrefixFilter
|
filter_class = filters.PrefixFilter
|
||||||
|
|
||||||
@@ -140,10 +140,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
available_prefixes.remove(allocated_prefix)
|
available_prefixes.remove(allocated_prefix)
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
context = {'request': request}
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
|
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
|
serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
|
||||||
|
|
||||||
# Create the new Prefix(es)
|
# Create the new Prefix(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@@ -199,10 +200,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
context = {'request': request}
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
|
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
|
serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
|
||||||
|
|
||||||
# Create the new IP address(es)
|
# Create the new IP address(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@@ -243,7 +245,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = IPAddress.objects.select_related(
|
queryset = IPAddress.objects.select_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine'
|
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'nat_outside'
|
'nat_outside', 'tags',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
filter_class = filters.IPAddressFilter
|
filter_class = filters.IPAddressFilter
|
||||||
@@ -264,7 +266,7 @@ class VLANGroupViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANViewSet(CustomFieldModelViewSet):
|
class VLANViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('tags')
|
||||||
serializer_class = serializers.VLANSerializer
|
serializer_class = serializers.VLANSerializer
|
||||||
filter_class = filters.VLANFilter
|
filter_class = filters.VLANFilter
|
||||||
|
|
||||||
@@ -274,6 +276,6 @@ class VLANViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ServiceViewSet(ModelViewSet):
|
class ServiceViewSet(ModelViewSet):
|
||||||
queryset = Service.objects.select_related('device')
|
queryset = Service.objects.select_related('device').prefetch_related('tags')
|
||||||
serializer_class = serializers.ServiceSerializer
|
serializer_class = serializers.ServiceSerializer
|
||||||
filter_class = filters.ServiceFilter
|
filter_class = filters.ServiceFilter
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ IPADDRESS_ROLE_CHOICES = (
|
|||||||
(IPADDRESS_ROLE_CARP, 'CARP'),
|
(IPADDRESS_ROLE_CARP, 'CARP'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IPADDRESS_ROLES_NONUNIQUE = (
|
||||||
|
# IPAddress roles which are exempt from unique address enforcement
|
||||||
|
IPADDRESS_ROLE_ANYCAST,
|
||||||
|
IPADDRESS_ROLE_VIP,
|
||||||
|
IPADDRESS_ROLE_VRRP,
|
||||||
|
IPADDRESS_ROLE_HSRP,
|
||||||
|
IPADDRESS_ROLE_GLBP,
|
||||||
|
IPADDRESS_ROLE_CARP,
|
||||||
|
)
|
||||||
|
|
||||||
# VLAN statuses
|
# VLAN statuses
|
||||||
VLAN_STATUS_ACTIVE = 1
|
VLAN_STATUS_ACTIVE = 1
|
||||||
VLAN_STATUS_RESERVED = 2
|
VLAN_STATUS_RESERVED = 2
|
||||||
|
|||||||
@@ -596,7 +596,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
if self.address:
|
if self.address:
|
||||||
|
|
||||||
# Enforce unique IP space (if applicable)
|
# Enforce unique IP space (if applicable)
|
||||||
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
|
if self.role not in IPADDRESS_ROLES_NONUNIQUE and (
|
||||||
|
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
||||||
|
) or (
|
||||||
|
self.vrf and self.vrf.enforce_unique
|
||||||
|
):
|
||||||
duplicate_ips = self.get_duplicates()
|
duplicate_ips = self.get_duplicates()
|
||||||
if duplicate_ips:
|
if duplicate_ips:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
|||||||
@@ -494,7 +494,8 @@ class PrefixTest(APITestCase):
|
|||||||
|
|
||||||
def test_create_single_available_prefix(self):
|
def test_create_single_available_prefix(self):
|
||||||
|
|
||||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)
|
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||||
|
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
|
||||||
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
||||||
|
|
||||||
# Create four available prefixes with individual requests
|
# Create four available prefixes with individual requests
|
||||||
@@ -512,6 +513,7 @@ class PrefixTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
||||||
|
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||||
self.assertEqual(response.data['description'], data['description'])
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
# Try to create one more prefix
|
# Try to create one more prefix
|
||||||
@@ -562,7 +564,8 @@ class PrefixTest(APITestCase):
|
|||||||
|
|
||||||
def test_create_single_available_ip(self):
|
def test_create_single_available_ip(self):
|
||||||
|
|
||||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), is_pool=True)
|
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||||
|
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
|
||||||
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||||
|
|
||||||
# Create all four available IPs with individual requests
|
# Create all four available IPs with individual requests
|
||||||
@@ -572,6 +575,7 @@ class PrefixTest(APITestCase):
|
|||||||
}
|
}
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||||
self.assertEqual(response.data['description'], data['description'])
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
# Try to create one more IP
|
# Try to create one more IP
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import netaddr
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from ipam.constants import IPADDRESS_ROLE_VIP
|
||||||
from ipam.models import IPAddress, Prefix, VRF
|
from ipam.models import IPAddress, Prefix, VRF
|
||||||
|
|
||||||
|
|
||||||
@@ -59,3 +60,8 @@ class TestIPAddress(TestCase):
|
|||||||
IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||||
|
|
||||||
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
|
||||||
|
def test_duplicate_nonunique_role(self):
|
||||||
|
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP)
|
||||||
|
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP)
|
||||||
|
|||||||
30
netbox/netbox/admin.py
Normal file
30
netbox/netbox/admin.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.admin import AdminSite
|
||||||
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||||
|
from taggit.admin import TagAdmin
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
|
||||||
|
class NetBoxAdminSite(AdminSite):
|
||||||
|
"""
|
||||||
|
Custom admin site
|
||||||
|
"""
|
||||||
|
site_header = 'NetBox Administration'
|
||||||
|
site_title = 'NetBox'
|
||||||
|
site_url = '/{}'.format(settings.BASE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
admin_site = NetBoxAdminSite(name='admin')
|
||||||
|
|
||||||
|
# Register external models
|
||||||
|
admin_site.register(Group, GroupAdmin)
|
||||||
|
admin_site.register(User, UserAdmin)
|
||||||
|
admin_site.register(Tag, TagAdmin)
|
||||||
|
|
||||||
|
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
||||||
|
try:
|
||||||
|
import django_rq
|
||||||
|
admin_site.index_template = 'django_rq/index.html'
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from rest_framework import authentication, exceptions
|
from rest_framework import authentication, exceptions
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
|
from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
|
||||||
@@ -104,8 +105,6 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
|||||||
|
|
||||||
def get_limit(self, request):
|
def get_limit(self, request):
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
if self.limit_query_param:
|
if self.limit_query_param:
|
||||||
try:
|
try:
|
||||||
limit = int(request.query_params[self.limit_query_param])
|
limit = int(request.query_params[self.limit_query_param])
|
||||||
@@ -123,6 +122,22 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
|||||||
|
|
||||||
return self.default_limit
|
return self.default_limit
|
||||||
|
|
||||||
|
def get_next_link(self):
|
||||||
|
|
||||||
|
# Pagination has been disabled
|
||||||
|
if not self.limit:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return super(OptionalLimitOffsetPagination, self).get_next_link()
|
||||||
|
|
||||||
|
def get_previous_link(self):
|
||||||
|
|
||||||
|
# Pagination has been disabled
|
||||||
|
if not self.limit:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return super(OptionalLimitOffsetPagination, self).get_previous_link()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ OBJ_TYPE_CHOICES = (
|
|||||||
('DCIM', (
|
('DCIM', (
|
||||||
('site', 'Sites'),
|
('site', 'Sites'),
|
||||||
('rack', 'Racks'),
|
('rack', 'Racks'),
|
||||||
|
('rackgroup', 'Rack Groups'),
|
||||||
('devicetype', 'Device types'),
|
('devicetype', 'Device types'),
|
||||||
('device', 'Devices'),
|
('device', 'Devices'),
|
||||||
('virtualchassis', 'Virtual Chassis'),
|
('virtualchassis', 'Virtual Chassis'),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
VERSION = '2.4.0'
|
VERSION = '2.4.5'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -272,7 +272,6 @@ RQ_QUEUES = {
|
|||||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RQ_SHOW_ADMIN_LINK = True
|
|
||||||
|
|
||||||
# drf_yasg settings for Swagger
|
# drf_yasg settings for Swagger
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
|
|
||||||
from netbox.views import APIRootView, HomeView, SearchView
|
from netbox.views import APIRootView, HomeView, SearchView
|
||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
|
from .admin import admin_site
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
openapi.Info(
|
openapi.Info(
|
||||||
@@ -60,7 +60,7 @@ _patterns = [
|
|||||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
|
|
||||||
# Admin
|
# Admin
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin_site.urls),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -69,7 +69,6 @@ if settings.WEBHOOKS_ENABLED:
|
|||||||
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
|
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
_patterns += [
|
_patterns += [
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ from rest_framework.views import APIView
|
|||||||
from circuits.filters import CircuitFilter, ProviderFilter
|
from circuits.filters import CircuitFilter, ProviderFilter
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
from circuits.tables import CircuitTable, ProviderTable
|
from circuits.tables import CircuitTable, ProviderTable
|
||||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter, VirtualChassisFilter
|
from dcim.filters import (
|
||||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site, VirtualChassis
|
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
|
||||||
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable, VirtualChassisTable
|
)
|
||||||
|
from dcim.models import (
|
||||||
|
ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site,
|
||||||
|
VirtualChassis
|
||||||
|
)
|
||||||
|
from dcim.tables import (
|
||||||
|
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
|
||||||
|
)
|
||||||
from extras.models import ObjectChange, ReportResult, TopologyMap
|
from extras.models import ObjectChange, ReportResult, TopologyMap
|
||||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||||
@@ -58,6 +65,12 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'table': RackTable,
|
'table': RackTable,
|
||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
|
('rackgroup', {
|
||||||
|
'queryset': RackGroup.objects.select_related('site').annotate(rack_count=Count('racks')),
|
||||||
|
'filter': RackGroupFilter,
|
||||||
|
'table': RackGroupTable,
|
||||||
|
'url': 'dcim:rackgroup_list',
|
||||||
|
}),
|
||||||
('devicetype', {
|
('devicetype', {
|
||||||
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
||||||
'filter': DeviceTypeFilter,
|
'filter': DeviceTypeFilter,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($(parent).val() || $(parent).attr('nullable') == 'true') {
|
if ($(parent).val() || $(parent).attr('nullable') == 'true') {
|
||||||
var api_url = child_field.attr('api-url') + '&limit=1000';
|
var api_url = child_field.attr('api-url');
|
||||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||||
var initial_value = child_field.attr('initial');
|
var initial_value = child_field.attr('initial');
|
||||||
var display_field = child_field.attr('display-field') || 'name';
|
var display_field = child_field.attr('display-field') || 'name';
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from .forms import ActivateUserKeyForm
|
from .forms import ActivateUserKeyForm
|
||||||
from .models import UserKey
|
from .models import UserKey
|
||||||
|
|
||||||
|
|
||||||
@admin.register(UserKey)
|
@admin.register(UserKey, site=admin_site)
|
||||||
class UserKeyAdmin(admin.ModelAdmin):
|
class UserKeyAdmin(admin.ModelAdmin):
|
||||||
actions = ['activate_selected']
|
actions = ['activate_selected']
|
||||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class SecretViewSet(ModelViewSet):
|
|||||||
queryset = Secret.objects.select_related(
|
queryset = Secret.objects.select_related(
|
||||||
'device__primary_ip4', 'device__primary_ip6', 'role',
|
'device__primary_ip4', 'device__primary_ip6', 'role',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'role__users', 'role__groups',
|
'role__users', 'role__groups', 'tags',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
filter_class = filters.SecretFilter
|
filter_class = filters.SecretFilter
|
||||||
|
|||||||
@@ -54,7 +54,9 @@
|
|||||||
<a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
|
<a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">
|
||||||
|
Inventory <span class="badge">{{ device.inventory_items.count }}</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.dcim.napalm_read %}
|
{% if perms.dcim.napalm_read %}
|
||||||
{% if device.status != 1 %}
|
{% if device.status != 1 %}
|
||||||
@@ -445,7 +447,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% if device_bays or device.device_type.is_parent_device %}
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
{% if perms.dcim.delete_devicebay %}
|
{% if perms.dcim.delete_devicebay %}
|
||||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -481,7 +483,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if device_bays and perms.dcim.delete_devicebay %}
|
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -553,7 +555,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -573,7 +575,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports or device.device_type.is_console_server %}
|
{% if cs_ports or device.device_type.is_console_server %}
|
||||||
{% if perms.dcim.delete_consoleserverport %}
|
{% if perms.dcim.delete_consoleserverport %}
|
||||||
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -606,12 +608,12 @@
|
|||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<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
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
<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">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -631,7 +633,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets or device.device_type.is_pdu %}
|
{% if power_outlets or device.device_type.is_pdu %}
|
||||||
{% if perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.delete_poweroutlet %}
|
||||||
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -664,12 +666,12 @@
|
|||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<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
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
<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">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -77,6 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Local Config Context Data</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.local_context_data %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Tags</strong></div>
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
{% if table.rows %}
|
{% if table.rows %}
|
||||||
{% if edit_url %}
|
{% if edit_url %}
|
||||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning">
|
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if delete_url %}
|
{% if delete_url %}
|
||||||
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger">
|
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
<a href="{% url 'dcim:interface' pk=connected_iface.pk %}"><span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span></a>
|
||||||
</td>
|
</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif iface.circuit_termination %}
|
{% elif iface.circuit_termination %}
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
|
<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>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
<a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning">
|
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger">
|
<a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -134,7 +134,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>{{ connected_interface.name }}</td>
|
<td>
|
||||||
|
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Type</td>
|
<td>Type</td>
|
||||||
|
|||||||
@@ -14,11 +14,6 @@
|
|||||||
{% render_field form.mgmt_only %}
|
{% render_field form.mgmt_only %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><strong>Tags</strong></div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,6 +140,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant Groups</td>
|
||||||
|
<td>
|
||||||
|
{% if configcontext.tenant_groups.all %}
|
||||||
|
<ul>
|
||||||
|
{% for tenant_group in configcontext.tenant_groups.all %}
|
||||||
|
<li><a href="{{ tenant_group.get_absolute_url }}">{{ tenant_group }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenants</td>
|
<td>Tenants</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -16,6 +16,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Local Context</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if obj.local_context_data %}
|
||||||
|
<pre>{{ obj.local_context_data|render_json }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<span class="help-block">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
The local config context overwrites all source contexts.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Source Contexts</strong>
|
<strong>Source Contexts</strong>
|
||||||
|
|||||||
@@ -194,10 +194,13 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% empty %}
|
{% if forloop.last %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item text-right">
|
||||||
Welcome to NetBox! {% if perms.add_site %} <a href="{% url 'dcim:site_add' %}">Add a site</a> to get started.{% endif %}
|
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="list-group-item text-muted">No change history found</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% with custom_fields=obj.custom_fields %}
|
{% with custom_fields=obj.get_custom_fields %}
|
||||||
{% if custom_fields %}
|
{% if custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
<a href="{{ aggregate.get_absolute_url }}">VRF</a>
|
<a href="{{ vrf.get_absolute_url }}">VRF</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if obj.mode %}
|
{% if obj.mode %}
|
||||||
|
|||||||
@@ -282,12 +282,12 @@
|
|||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -315,9 +315,9 @@
|
|||||||
$('button.toggle-ips').click(function() {
|
$('button.toggle-ips').click(function() {
|
||||||
var selected = $(this).attr('selected');
|
var selected = $(this).attr('selected');
|
||||||
if (selected) {
|
if (selected) {
|
||||||
$('#interfaces_table tr.ipaddress').hide();
|
$('#interfaces_table tr.ipaddresses').hide();
|
||||||
} else {
|
} else {
|
||||||
$('#interfaces_table tr.ipaddress').show();
|
$('#interfaces_table tr.ipaddresses').show();
|
||||||
}
|
}
|
||||||
$(this).attr('selected', !selected);
|
$(this).attr('selected', !selected);
|
||||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Local Config Context Data</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.local_context_data %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Tags</strong></div>
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ class TenantGroupViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class TenantViewSet(CustomFieldModelViewSet):
|
class TenantViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Tenant.objects.select_related('group')
|
queryset = Tenant.objects.select_related('group').prefetch_related('tags')
|
||||||
serializer_class = serializers.TenantSerializer
|
serializer_class = serializers.TenantSerializer
|
||||||
filter_class = filters.TenantFilter
|
filter_class = filters.TenantFilter
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from .models import Token
|
from .models import Token
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ class TokenAdminForm(forms.ModelForm):
|
|||||||
model = Token
|
model = Token
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Token)
|
@admin.register(Token, site=admin_site)
|
||||||
class TokenAdmin(admin.ModelAdmin):
|
class TokenAdmin(admin.ModelAdmin):
|
||||||
form = TokenAdminForm
|
form = TokenAdminForm
|
||||||
list_display = ['key', 'user', 'created', 'expires', 'write_enabled', 'description']
|
list_display = ['key', 'user', 'created', 'expires', 'write_enabled', 'description']
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ class ChoiceField(Field):
|
|||||||
return {'value': obj, 'label': self._choices[obj]}
|
return {'value': obj, 'label': self._choices[obj]}
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
# Hotwiring boolean values
|
||||||
|
if hasattr(data, 'lower'):
|
||||||
|
if data.lower() == 'true':
|
||||||
|
return True
|
||||||
|
if data.lower() == 'false':
|
||||||
|
return False
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -102,10 +108,9 @@ class TimeZoneField(Field):
|
|||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if not data:
|
if not data:
|
||||||
return ""
|
return ""
|
||||||
try:
|
if data not in pytz.common_timezones:
|
||||||
return pytz.timezone(str(data))
|
raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
return pytz.timezone(data)
|
||||||
raise ValidationError('Invalid time zone "{}"'.format(data))
|
|
||||||
|
|
||||||
|
|
||||||
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
||||||
@@ -164,7 +169,9 @@ class WritableNestedSerializer(ModelSerializer):
|
|||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return self.Meta.model.objects.get(pk=data)
|
return self.Meta.model.objects.get(pk=int(data))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValidationError("Primary key must be an integer")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise ValidationError("Invalid ID")
|
raise ValidationError("Invalid ID")
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.forms import JSONField as _JSONField
|
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from mptt.forms import TreeNodeMultipleChoiceField
|
from mptt.forms import TreeNodeMultipleChoiceField
|
||||||
@@ -556,9 +557,11 @@ class JSONField(_JSONField):
|
|||||||
self.widget.attrs['placeholder'] = ''
|
self.widget.attrs['placeholder'] = ''
|
||||||
|
|
||||||
def prepare_value(self, value):
|
def prepare_value(self, value):
|
||||||
|
if isinstance(value, InvalidJSONInput):
|
||||||
|
return value
|
||||||
if value is None:
|
if value is None:
|
||||||
return ''
|
return ''
|
||||||
return super(JSONField, self).prepare_value(value)
|
return json.dumps(value, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ def serialize_object(obj, extra=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Include any tags
|
# Include any tags
|
||||||
# if hasattr(obj, 'tags'):
|
if hasattr(obj, 'tags'):
|
||||||
# data['tags'] = [tag.name for tag in obj.tags.all()]
|
data['tags'] = [tag.name for tag in obj.tags.all()]
|
||||||
|
|
||||||
# Append any extra data
|
# Append any extra data
|
||||||
if extra is not None:
|
if extra is not None:
|
||||||
|
|||||||
@@ -710,22 +710,17 @@ class ComponentCreateView(View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(form.cleaned_data)
|
data = deepcopy(request.POST)
|
||||||
|
data[self.parent_field] = parent.pk
|
||||||
|
|
||||||
for name in form.cleaned_data['name_pattern']:
|
for name in form.cleaned_data['name_pattern']:
|
||||||
component_data = {
|
|
||||||
self.parent_field: parent.pk,
|
# Initialize the individual component form
|
||||||
'name': name,
|
data['name'] = name
|
||||||
}
|
component_form = self.model_form(data)
|
||||||
# Replace objects with their primary key to keep component_form.clean() happy
|
|
||||||
for k, v in data.items():
|
|
||||||
if hasattr(v, 'pk'):
|
|
||||||
component_data[k] = v.pk
|
|
||||||
else:
|
|
||||||
component_data[k] = v
|
|
||||||
component_form = self.model_form(component_data)
|
|
||||||
if component_form.is_valid():
|
if component_form.is_valid():
|
||||||
new_components.append(component_form.save(commit=False))
|
new_components.append(component_form)
|
||||||
else:
|
else:
|
||||||
for field, errors in component_form.errors.as_data().items():
|
for field, errors in component_form.errors.as_data().items():
|
||||||
# Assign errors on the child form's name field to name_pattern on the parent form
|
# Assign errors on the child form's name field to name_pattern on the parent form
|
||||||
@@ -735,26 +730,10 @@ class ComponentCreateView(View):
|
|||||||
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
self.model.objects.bulk_create(new_components)
|
|
||||||
|
|
||||||
# ManyToMany relations are bulk created via the through model
|
# Create the new components
|
||||||
m2m_fields = [field for field in component_form.fields if type(component_form.fields[field]) in M2M_FIELD_TYPES]
|
for component_form in new_components:
|
||||||
if m2m_fields:
|
component_form.save()
|
||||||
for field in m2m_fields:
|
|
||||||
field_links = []
|
|
||||||
for new_component in new_components:
|
|
||||||
for related_obj in component_form.cleaned_data[field]:
|
|
||||||
# The through model columns are the id's of our M2M relation objects
|
|
||||||
through_kwargs = {}
|
|
||||||
new_component_column = new_component.__class__.__name__ + '_id'
|
|
||||||
related_obj_column = related_obj.__class__.__name__ + '_id'
|
|
||||||
through_kwargs.update({
|
|
||||||
new_component_column.lower(): new_component.id,
|
|
||||||
related_obj_column.lower(): related_obj.id
|
|
||||||
})
|
|
||||||
field_link = getattr(self.model, field).through(**through_kwargs)
|
|
||||||
field_links.append(field_link)
|
|
||||||
getattr(self.model, field).through.objects.bulk_create(field_links)
|
|
||||||
|
|
||||||
messages.success(request, "Added {} {} to {}.".format(
|
messages.success(request, "Added {} {} to {}.".format(
|
||||||
len(new_components), self.model._meta.verbose_name_plural, parent
|
len(new_components), self.model._meta.verbose_name_plural, parent
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class NestedClusterSerializer(WritableNestedSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||||
class VirtualMachineIPAddressSerializer(serializers.ModelSerializer):
|
class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -92,6 +92,7 @@ class VirtualMachineIPAddressSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||||
|
site = NestedSiteSerializer(read_only=True)
|
||||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
@@ -104,8 +105,9 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
|
||||||
'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'local_context_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
'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',
|
'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
|
'local_context_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
@@ -146,7 +149,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
|||||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
||||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ClusterGroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterViewSet(CustomFieldModelViewSet):
|
class ClusterViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Cluster.objects.select_related('type', 'group')
|
queryset = Cluster.objects.select_related('type', 'group').prefetch_related('tags')
|
||||||
serializer_class = serializers.ClusterSerializer
|
serializer_class = serializers.ClusterSerializer
|
||||||
filter_class = filters.ClusterFilter
|
filter_class = filters.ClusterFilter
|
||||||
|
|
||||||
@@ -45,7 +45,9 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineViewSet(CustomFieldModelViewSet):
|
class VirtualMachineViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VirtualMachine.objects.all()
|
queryset = VirtualMachine.objects.select_related(
|
||||||
|
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6'
|
||||||
|
).prefetch_related('tags')
|
||||||
filter_class = filters.VirtualMachineFilter
|
filter_class = filters.VirtualMachineFilter
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@@ -58,6 +60,8 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(ModelViewSet):
|
class InterfaceViewSet(ModelViewSet):
|
||||||
queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
|
queryset = Interface.objects.filter(
|
||||||
|
virtual_machine__isnull=False
|
||||||
|
).select_related('virtual_machine').prefetch_related('tags')
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filter_class = filters.InterfaceFilter
|
filter_class = filters.InterfaceFilter
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from taggit.forms import TagField
|
|||||||
|
|
||||||
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
|
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
|
||||||
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
||||||
from dcim.formfields import MACAddressFormField
|
|
||||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
@@ -17,7 +16,8 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
|
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
|
||||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea, add_blank_choice
|
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea,
|
||||||
|
add_blank_choice
|
||||||
)
|
)
|
||||||
from .constants import VM_STATUS_CHOICES
|
from .constants import VM_STATUS_CHOICES
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
@@ -247,6 +247,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tags = TagField(required=False)
|
tags = TagField(required=False)
|
||||||
|
local_context_data = JSONField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
@@ -254,6 +255,9 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||||
'vcpus', 'memory', 'disk', 'comments', 'tags',
|
'vcpus', 'memory', 'disk', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
help_texts = {
|
||||||
|
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@@ -415,11 +419,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
'virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags',
|
||||||
'untagged_vlan', 'tagged_vlans',
|
'untagged_vlan', 'tagged_vlans',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@@ -456,8 +461,9 @@ class InterfaceCreateForm(ComponentForm):
|
|||||||
form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES, initial=IFACE_FF_VIRTUAL, widget=forms.HiddenInput())
|
form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES, initial=IFACE_FF_VIRTUAL, widget=forms.HiddenInput())
|
||||||
enabled = forms.BooleanField(required=False)
|
enabled = forms.BooleanField(required=False)
|
||||||
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
|
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
|
||||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
mac_address = forms.CharField(required=False, label='MAC Address')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.8 on 2018-09-16 02:01
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0007_change_logging'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='local_context_data',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -260,6 +260,22 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate primary IP addresses
|
||||||
|
interfaces = self.interfaces.all()
|
||||||
|
for field in ['primary_ip4', 'primary_ip6']:
|
||||||
|
ip = getattr(self, field)
|
||||||
|
if ip is not None:
|
||||||
|
if ip.interface in interfaces:
|
||||||
|
pass
|
||||||
|
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValidationError({
|
||||||
|
field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
|
||||||
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.name,
|
self.name,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from netaddr import IPNetwork
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED
|
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from ipam.models import VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
|
|
||||||
@@ -367,6 +368,10 @@ class VirtualMachineTest(APITestCase):
|
|||||||
|
|
||||||
def test_update_virtualmachine(self):
|
def test_update_virtualmachine(self):
|
||||||
|
|
||||||
|
interface = Interface.objects.create(name='Test Interface 1', virtual_machine=self.virtualmachine1)
|
||||||
|
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
|
||||||
|
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
|
||||||
|
|
||||||
cluster2 = Cluster.objects.create(
|
cluster2 = Cluster.objects.create(
|
||||||
name='Test Cluster 2',
|
name='Test Cluster 2',
|
||||||
type=ClusterType.objects.first(),
|
type=ClusterType.objects.first(),
|
||||||
@@ -375,6 +380,8 @@ class VirtualMachineTest(APITestCase):
|
|||||||
data = {
|
data = {
|
||||||
'name': 'Test Virtual Machine X',
|
'name': 'Test Virtual Machine X',
|
||||||
'cluster': cluster2.pk,
|
'cluster': cluster2.pk,
|
||||||
|
'primary_ip4': ip4_address.pk,
|
||||||
|
'primary_ip6': ip6_address.pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': self.virtualmachine1.pk})
|
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': self.virtualmachine1.pk})
|
||||||
@@ -385,6 +392,8 @@ class VirtualMachineTest(APITestCase):
|
|||||||
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
|
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(virtualmachine1.name, data['name'])
|
self.assertEqual(virtualmachine1.name, data['name'])
|
||||||
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
|
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
|
||||||
|
self.assertEqual(virtualmachine1.primary_ip4.pk, data['primary_ip4'])
|
||||||
|
self.assertEqual(virtualmachine1.primary_ip6.pk, data['primary_ip6'])
|
||||||
|
|
||||||
def test_delete_virtualmachine(self):
|
def test_delete_virtualmachine(self):
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -183,17 +184,21 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_pks = form.cleaned_data['devices']
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
# Assign the selected Devices to the Cluster
|
# Assign the selected Devices to the Cluster
|
||||||
devices = form.cleaned_data['devices']
|
for device in Device.objects.filter(pk__in=device_pks):
|
||||||
Device.objects.filter(pk__in=devices).update(cluster=cluster)
|
device.cluster = cluster
|
||||||
|
device.save()
|
||||||
|
|
||||||
messages.success(request, "Added {} devices to cluster {}".format(
|
messages.success(request, "Added {} devices to cluster {}".format(
|
||||||
len(devices), cluster
|
len(device_pks), cluster
|
||||||
))
|
))
|
||||||
return redirect(cluster.get_absolute_url())
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'cluser': cluster,
|
'cluster': cluster,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': cluster.get_absolute_url(),
|
'return_url': cluster.get_absolute_url(),
|
||||||
})
|
})
|
||||||
@@ -212,12 +217,16 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
|
|||||||
form = self.form(request.POST)
|
form = self.form(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_pks = form.cleaned_data['pk']
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
# Remove the selected Devices from the Cluster
|
# Remove the selected Devices from the Cluster
|
||||||
devices = form.cleaned_data['pk']
|
for device in Device.objects.filter(pk__in=device_pks):
|
||||||
Device.objects.filter(pk__in=devices).update(cluster=None)
|
device.cluster = None
|
||||||
|
device.save()
|
||||||
|
|
||||||
messages.success(request, "Removed {} devices from cluster {}".format(
|
messages.success(request, "Removed {} devices from cluster {}".format(
|
||||||
len(devices), cluster
|
len(device_pks), cluster
|
||||||
))
|
))
|
||||||
return redirect(cluster.get_absolute_url())
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user