diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b01cb6dc8..38a6fd550 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,7 +5,9 @@ about: Report a reproducible bug in the current release of NetBox --- ### Environment -* Python version: -* NetBox version: +* Python version: +* NetBox version: @@ -14,5 +16,13 @@ about: Suggest an addition or modification to the NetBox documentation [ ] Deprecation [ ] Cleanup (formatting, typos, etc.) +### Area +[ ] Installation instructions +[ ] Configuration parameters +[ ] Functionality/features +[ ] REST API +[ ] Administration/development +[ ] Other + ### Proposed Changes diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ebe19d811..2f742d416 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,7 +5,9 @@ about: Propose a new NetBox feature or enhancement --- ### Environment -* Python version: -* NetBox version: +* Python version: +* NetBox version: ### Proposed Changes diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index bc79e90ab..8cadddeb5 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -90,6 +90,14 @@ This setting enables debugging. This should be done only during development or t --- +## DEVELOPER + +Default: False + +This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. + +--- + ## EMAIL In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting: @@ -127,7 +135,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*'] --- -# ENFORCE_GLOBAL_UNIQUE +## ENFORCE_GLOBAL_UNIQUE Default: False diff --git a/docs/installation/3-http-daemon.md b/docs/installation/3-http-daemon.md index 4ca566aa3..cc1065fef 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/3-http-daemon.md @@ -29,7 +29,7 @@ server { location / { proxy_pass http://127.0.0.1:8001; - proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } @@ -107,9 +107,10 @@ Install gunicorn: # pip3 install gunicorn ``` -Copy `contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. +Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. ```no-highlight +# cd /opt/netbox # cp contrib/gunicorn.py /opt/netbox/gunicorn.py ``` diff --git a/docs/installation/4-ldap.md b/docs/installation/4-ldap.md index a41400808..953d3cb28 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/4-ldap.md @@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = { AUTH_LDAP_FIND_GROUP_PERMS = True # Cache groups for one hour to reduce LDAP traffic -AUTH_LDAP_CACHE_GROUPS = True -AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 +AUTH_LDAP_CACHE_TIMEOUT = 3600 + ``` * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. diff --git a/docs/installation/migrating-to-systemd.md b/docs/installation/migrating-to-systemd.md index 6199b5511..f5fcb7598 100644 --- a/docs/installation/migrating-to-systemd.md +++ b/docs/installation/migrating-to-systemd.md @@ -12,84 +12,19 @@ Migration is not required, as supervisord will still continue to function. ### systemd configuration: -Copy or link contrib/netbox.service and contrib/netbox-rq.service to /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service +We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory: ```no-highlight -# cp contrib/netbox.service /etc/systemd/system/netbox.service -# cp contrib/netbox-rq.service /etc/systemd/system/netbox-rq.service +# cp contrib/*.service /etc/systemd/system/ ``` -Edit /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service. Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`). If using CentOS/RHEL. Change the username from `www-data` to `nginx` or `apache`: +!!! note + These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files. -```no-highlight -/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi -``` +!!! note + You may need to modify the user that the systemd service runs as. Please verify the user for httpd on your specific release and edit both files to match your httpd service under user and group. The username could be "nobody", "nginx", "apache", "www-data" or any number of other usernames. -```no-highlight -User=www-data -Group=www-data -``` - -Copy contrib/netbox.env to /etc/sysconfig/netbox.env - -```no-highlight -# cp contrib/netbox.env /etc/sysconfig/netbox.env -``` - -Edit /etc/sysconfig/netbox.env and change the settings as required. Update the `WorkingDirectory` variable if needed. - -```no-highlight -# Name is the Process Name -# -Name = 'Netbox' - -# ConfigPath is the path to the gunicorn config file. -# -ConfigPath=/opt/netbox/gunicorn.conf - -# WorkingDirectory is the Working Directory for Netbox. -# -WorkingDirectory=/opt/netbox/ - -# PidPath is the path to the pid for the netbox WSGI -# -PidPath=/var/run/netbox.pid -``` - -Copy contrib/gunicorn.conf to gunicorn.conf - -```no-highlight -# cp contrib/gunicorn.conf to gunicorn.conf -``` - -Edit gunicorn.conf and change the settings as required. - -``` -# Bind is the ip and port that the Netbox WSGI should bind to -# -bind='127.0.0.1:8001' - -# Workers is the number of workers that GUnicorn should spawn. -# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17. -# -workers=3 - -# Threads -# The number of threads for handling requests -# -threads=3 - -# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error) -# -timeout=120 - -# ErrorLog -# ErrorLog is the logfile for the ErrorLog -# -errorlog='/opt/netbox/netbox.log' -``` - -Finally, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: +Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: ```no-highlight # systemctl daemon-reload @@ -98,3 +33,25 @@ Finally, start the `netbox` and `netbox-rq` services and enable them to initiate # systemctl enable netbox.service # systemctl enable netbox-rq.service ``` + +You can use the command `systemctl status netbox` to verify that the WSGI service is running: + +``` +# systemctl status netbox.service +● netbox.service - NetBox WSGI Service + Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled) + Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago + Docs: https://netbox.readthedocs.io/en/stable/ + Main PID: 11993 (gunicorn) + Tasks: 6 (limit: 2362) + CGroup: /system.slice/netbox.service + ├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... + ├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... + ├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... +... +``` + +At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. + +!!! info + Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment. diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 130e3c0c0..cbdc47de8 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,15 +1,67 @@ -# v2.7.3 (FUTURE) +# v2.7.5 (FUTURE) + +## Enhancements + +* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable +* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components + +## Bug Fixes + +* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional +* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view +* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form +* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list + +# v2.7.4 (2020-02-04) + +## Enhancements + +* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget +* [#3313](https://github.com/netbox-community/netbox/issues/3313) - Toggle config context display between JSON and YAML +* [#3886](https://github.com/netbox-community/netbox/issues/3886) - Enable assigning config contexts by cluster and cluster group +* [#4051](https://github.com/netbox-community/netbox/issues/4051) - Disable the `makemigrations` management command + +## Bug Fixes + +* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when bulk editing interfaces (revised) +* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts +* [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer +* [#4052](https://github.com/netbox-community/netbox/issues/4052) - Fix error when bulk importing interfaces to virtual machines +* [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569) +* [#4067](https://github.com/netbox-community/netbox/issues/4067) - Correct permission checked when creating a rack (vs. editing) +* [#4071](https://github.com/netbox-community/netbox/issues/4071) - Enforce "view tag" permission on individual tag view +* [#4079](https://github.com/netbox-community/netbox/issues/4079) - Fix assignment of power panel when bulk editing power feeds +* [#4084](https://github.com/netbox-community/netbox/issues/4084) - Fix exception when creating an interface with tagged VLANs + +--- + +# v2.7.3 (2020-01-28) ## Enhancements * [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable +* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits * [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts +* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP +* [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps ## Bug Fixes +* [#3950](https://github.com/netbox-community/netbox/issues/3950) - Automatically select parent manufacturer when specifying initial device type during device creation +* [#3982](https://github.com/netbox-community/netbox/issues/3982) - Restore tooltip for reservations on rack elevations * [#3983](https://github.com/netbox-community/netbox/issues/3983) - Permit the creation of multiple unnamed devices * [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks -* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Fixed overflowing dropdown menus becoming unreachable +* [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank +* [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings +* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form +* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations +* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces +* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places) +* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status +* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs +* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk +* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms --- diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index b22135b3f..6bac48a59 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,11 +3,11 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie from circuits.choices import CircuitStatusChoices from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer +from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import ConnectedEndpointSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer -from utilities.api import ChoiceField, ValidatedModelSerializer +from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer from .nested_serializers import * @@ -39,18 +39,30 @@ class CircuitTypeSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug', 'description', 'circuit_count'] +class CircuitCircuitTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + site = NestedSiteSerializer() + connected_endpoint = NestedInterfaceSerializer() + + class Meta: + model = CircuitTermination + fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id'] + + class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer): provider = NestedProviderSerializer() status = ChoiceField(choices=CircuitStatusChoices, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) + termination_a = CircuitCircuitTerminationSerializer(read_only=True) + termination_z = CircuitCircuitTerminationSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = Circuit fields = [ 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index b9d1b439b..cd3015d0a 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -15,15 +15,15 @@ router = routers.DefaultRouter() router.APIRootView = CircuitsRootView # Field choices -router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') # Providers -router.register(r'providers', views.ProviderViewSet) +router.register('providers', views.ProviderViewSet) # Circuits -router.register(r'circuit-types', views.CircuitTypeViewSet) -router.register(r'circuits', views.CircuitViewSet) -router.register(r'circuit-terminations', views.CircuitTerminationViewSet) +router.register('circuit-types', views.CircuitTypeViewSet) +router.register('circuits', views.CircuitViewSet) +router.register('circuit-terminations', views.CircuitTerminationViewSet) app_name = 'circuits-api' urlpatterns = router.urls diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 98b7c9184..75f7e0e3e 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -62,7 +62,9 @@ class CircuitTypeViewSet(ModelViewSet): # class CircuitViewSet(CustomFieldModelViewSet): - queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags') + queryset = Circuit.objects.prefetch_related( + 'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device' + ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer filterset_class = filters.CircuitFilterSet diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index d5d78e7bd..caf8d9d36 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,12 +2,14 @@ from django import forms from taggit.forms import TagField from dcim.models import Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, +) from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, - DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple + APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, + FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField ) from .choices import CircuitStatusChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -17,7 +19,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider # Providers # -class ProviderForm(BootstrapMixin, CustomFieldForm): +class ProviderForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() comments = CommentField() tags = TagField( @@ -46,7 +48,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm): } -class ProviderCSVForm(forms.ModelForm): +class ProviderCSVForm(CustomFieldModelCSVForm): slug = SlugField() class Meta: @@ -89,7 +91,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi label='Admin contact' ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -128,6 +131,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='ASN' ) + tag = TagFilterField(model) # @@ -159,7 +163,7 @@ class CircuitTypeCSVForm(forms.ModelForm): # Circuits # -class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): comments = CommentField() tags = TagField( required=False @@ -187,7 +191,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class CircuitCSVForm(forms.ModelForm): +class CircuitCSVForm(CustomFieldModelCSVForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', @@ -332,6 +336,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm min_value=0, label='Commit rate (Kbps)' ) + tag = TagFilterField(model) # diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 576437ef1..d2cb8e5ab 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,23 +1,15 @@ -import urllib.parse - -from django.test import Client, TestCase -from django.urls import reverse +import datetime +from circuits.choices import * from circuits.models import Circuit, CircuitType, Provider -from utilities.testing import create_test_user +from utilities.testing import StandardTestCases -class ProviderTestCase(TestCase): +class ProviderTestCase(StandardTestCases.Views): + model = Provider - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_provider', - 'circuits.add_provider', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Provider.objects.bulk_create([ Provider(name='Provider 1', slug='provider-1', asn=65001), @@ -25,48 +17,45 @@ class ProviderTestCase(TestCase): Provider(name='Provider 3', slug='provider-3', asn=65003), ]) - def test_provider_list(self): - - url = reverse('circuits:provider_list') - params = { - "q": "test", + cls.form_data = { + 'name': 'Provider X', + 'slug': 'provider-x', + 'asn': 65123, + 'account': '1234', + 'portal_url': 'http://example.com/portal', + 'noc_contact': 'noc@example.com', + 'admin_contact': 'admin@example.com', + 'comments': 'Another provider', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_provider(self): - - provider = Provider.objects.first() - response = self.client.get(provider.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_provider_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Provider 4,provider-4", "Provider 5,provider-5", "Provider 6,provider-6", ) - response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Provider.objects.count(), 6) + cls.bulk_edit_data = { + 'asn': 65009, + 'account': '5678', + 'portal_url': 'http://example.com/portal2', + 'noc_contact': 'noc2@example.com', + 'admin_contact': 'admin2@example.com', + 'comments': 'New comments', + } -class CircuitTypeTestCase(TestCase): +class CircuitTypeTestCase(StandardTestCases.Views): + model = CircuitType - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_circuittype', - 'circuits.add_circuittype', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): CircuitType.objects.bulk_create([ CircuitType(name='Circuit Type 1', slug='circuit-type-1'), @@ -74,79 +63,71 @@ class CircuitTypeTestCase(TestCase): CircuitType(name='Circuit Type 3', slug='circuit-type-3'), ]) - def test_circuittype_list(self): + cls.form_data = { + 'name': 'Circuit Type X', + 'slug': 'circuit-type-x', + 'description': 'A new circuit type', + } - url = reverse('circuits:circuittype_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_circuittype_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Circuit Type 4,circuit-type-4", "Circuit Type 5,circuit-type-5", "Circuit Type 6,circuit-type-6", ) - response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(CircuitType.objects.count(), 6) +class CircuitTestCase(StandardTestCases.Views): + model = Circuit + @classmethod + def setUpTestData(cls): -class CircuitTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_circuit', - 'circuits.add_circuit', - ] + providers = ( + Provider(name='Provider 1', slug='provider-1', asn=65001), + Provider(name='Provider 2', slug='provider-2', asn=65002), ) - self.client = Client() - self.client.force_login(user) + Provider.objects.bulk_create(providers) - provider = Provider(name='Provider 1', slug='provider-1', asn=65001) - provider.save() - - circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1') - circuittype.save() + circuittypes = ( + CircuitType(name='Circuit Type 1', slug='circuit-type-1'), + CircuitType(name='Circuit Type 2', slug='circuit-type-2'), + ) + CircuitType.objects.bulk_create(circuittypes) Circuit.objects.bulk_create([ - Circuit(cid='Circuit 1', provider=provider, type=circuittype), - Circuit(cid='Circuit 2', provider=provider, type=circuittype), - Circuit(cid='Circuit 3', provider=provider, type=circuittype), + Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]), + Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]), + Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]), ]) - def test_circuit_list(self): - - url = reverse('circuits:circuit_list') - params = { - "provider": Provider.objects.first().slug, - "type": CircuitType.objects.first().slug, + cls.form_data = { + 'cid': 'Circuit X', + 'provider': providers[1].pk, + 'type': circuittypes[1].pk, + 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, + 'tenant': None, + 'install_date': datetime.date(2020, 1, 1), + 'commit_rate': 1000, + 'description': 'A new circuit', + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_circuit(self): - - circuit = Circuit.objects.first() - response = self.client.get(circuit.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_circuit_import(self): - - csv_data = ( + cls.csv_data = ( "cid,provider,type", "Circuit 4,Provider 1,Circuit Type 1", "Circuit 5,Provider 1,Circuit Type 1", "Circuit 6,Provider 1,Circuit Type 1", ) - response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)}) + cls.bulk_edit_data = { + 'provider': providers[1].pk, + 'type': circuittypes[1].pk, + 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, + 'tenant': None, + 'commit_rate': 2000, + 'description': 'New description', + 'comments': 'New comments', - self.assertEqual(response.status_code, 200) - self.assertEqual(Circuit.objects.count(), 6) + } diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index c142a831a..72d9720df 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -9,42 +9,42 @@ app_name = 'circuits' urlpatterns = [ # Providers - path(r'providers/', views.ProviderListView.as_view(), name='provider_list'), - path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), - path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), - path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), - path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), - path(r'providers//', views.ProviderView.as_view(), name='provider'), - path(r'providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), - path(r'providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), - path(r'providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), + path('providers/', views.ProviderListView.as_view(), name='provider_list'), + path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), + path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), + path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), + path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), + path('providers//', views.ProviderView.as_view(), name='provider'), + path('providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), + path('providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), + path('providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), # Circuit types - path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), - path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), - path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), - path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), - path(r'circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), - path(r'circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), + path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), + path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), + path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), + path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), + path('circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), + path('circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), # Circuits - path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'), - path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), - path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), - path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), - path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), - path(r'circuits//', views.CircuitView.as_view(), name='circuit'), - path(r'circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), - path(r'circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), - path(r'circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), - path(r'circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), + path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), + path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), + path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), + path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), + path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), + path('circuits//', views.CircuitView.as_view(), name='circuit'), + path('circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), + path('circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), + path('circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), + path('circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations - path(r'circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), - path(r'circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), - path(r'circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), - path(r'circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), - path(r'circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), + path('circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), + path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), + path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), + path('circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), + path('circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), ] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index fd55d9b05..5a915becc 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -15,65 +15,65 @@ router = routers.DefaultRouter() router.APIRootView = DCIMRootView # Field choices -router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') # Sites -router.register(r'regions', views.RegionViewSet) -router.register(r'sites', views.SiteViewSet) +router.register('regions', views.RegionViewSet) +router.register('sites', views.SiteViewSet) # Racks -router.register(r'rack-groups', views.RackGroupViewSet) -router.register(r'rack-roles', views.RackRoleViewSet) -router.register(r'racks', views.RackViewSet) -router.register(r'rack-reservations', views.RackReservationViewSet) +router.register('rack-groups', views.RackGroupViewSet) +router.register('rack-roles', views.RackRoleViewSet) +router.register('racks', views.RackViewSet) +router.register('rack-reservations', views.RackReservationViewSet) # Device types -router.register(r'manufacturers', views.ManufacturerViewSet) -router.register(r'device-types', views.DeviceTypeViewSet) +router.register('manufacturers', views.ManufacturerViewSet) +router.register('device-types', views.DeviceTypeViewSet) # Device type components -router.register(r'console-port-templates', views.ConsolePortTemplateViewSet) -router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet) -router.register(r'power-port-templates', views.PowerPortTemplateViewSet) -router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet) -router.register(r'interface-templates', views.InterfaceTemplateViewSet) -router.register(r'front-port-templates', views.FrontPortTemplateViewSet) -router.register(r'rear-port-templates', views.RearPortTemplateViewSet) -router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet) +router.register('console-port-templates', views.ConsolePortTemplateViewSet) +router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet) +router.register('power-port-templates', views.PowerPortTemplateViewSet) +router.register('power-outlet-templates', views.PowerOutletTemplateViewSet) +router.register('interface-templates', views.InterfaceTemplateViewSet) +router.register('front-port-templates', views.FrontPortTemplateViewSet) +router.register('rear-port-templates', views.RearPortTemplateViewSet) +router.register('device-bay-templates', views.DeviceBayTemplateViewSet) # Devices -router.register(r'device-roles', views.DeviceRoleViewSet) -router.register(r'platforms', views.PlatformViewSet) -router.register(r'devices', views.DeviceViewSet) +router.register('device-roles', views.DeviceRoleViewSet) +router.register('platforms', views.PlatformViewSet) +router.register('devices', views.DeviceViewSet) # Device components -router.register(r'console-ports', views.ConsolePortViewSet) -router.register(r'console-server-ports', views.ConsoleServerPortViewSet) -router.register(r'power-ports', views.PowerPortViewSet) -router.register(r'power-outlets', views.PowerOutletViewSet) -router.register(r'interfaces', views.InterfaceViewSet) -router.register(r'front-ports', views.FrontPortViewSet) -router.register(r'rear-ports', views.RearPortViewSet) -router.register(r'device-bays', views.DeviceBayViewSet) -router.register(r'inventory-items', views.InventoryItemViewSet) +router.register('console-ports', views.ConsolePortViewSet) +router.register('console-server-ports', views.ConsoleServerPortViewSet) +router.register('power-ports', views.PowerPortViewSet) +router.register('power-outlets', views.PowerOutletViewSet) +router.register('interfaces', views.InterfaceViewSet) +router.register('front-ports', views.FrontPortViewSet) +router.register('rear-ports', views.RearPortViewSet) +router.register('device-bays', views.DeviceBayViewSet) +router.register('inventory-items', views.InventoryItemViewSet) # Connections -router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') -router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections') -router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') +router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') +router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections') +router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') # Cables -router.register(r'cables', views.CableViewSet) +router.register('cables', views.CableViewSet) # Virtual chassis -router.register(r'virtual-chassis', views.VirtualChassisViewSet) +router.register('virtual-chassis', views.VirtualChassisViewSet) # Power -router.register(r'power-panels', views.PowerPanelViewSet) -router.register(r'power-feeds', views.PowerFeedViewSet) +router.register('power-panels', views.PowerPanelViewSet) +router.register('power-feeds', views.PowerFeedViewSet) # Miscellaneous -router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device') +router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device') app_name = 'dcim-api' urlpatterns = router.urls diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 3a6f8e5e9..0e05867e4 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -4,17 +4,30 @@ from .choices import InterfaceTypeChoices # -# Rack elevation rendering +# Racks # +RACK_U_HEIGHT_DEFAULT = 42 + RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230 RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20 # -# Interface type groups +# RearPorts # +REARPORT_POSITIONS_MIN = 1 +REARPORT_POSITIONS_MAX = 64 + + +# +# Interfaces +# + +INTERFACE_MTU_MIN = 1 +INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer + VIRTUAL_IFACE_TYPES = [ InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_LAG, @@ -31,6 +44,17 @@ WIRELESS_IFACE_TYPES = [ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES +# +# PowerFeeds +# + +POWERFEED_VOLTAGE_DEFAULT = 120 + +POWERFEED_AMPERAGE_DEFAULT = 20 + +POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage + + # # Cabling and connections # diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json deleted file mode 100644 index b9f41edb5..000000000 --- a/netbox/dcim/fixtures/dcim.json +++ /dev/null @@ -1,5732 +0,0 @@ -[ -{ - "model": "dcim.site", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "TEST1", - "slug": "test1", - "facility": "Test Facility", - "asn": 65535, - "physical_address": "555 Test Ave.\r\nTest, NY 55555", - "shipping_address": "", - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R1", - "facility_id": "T23A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R2", - "facility_id": "T24A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 1, - "fields": { - "name": "Juniper", - "slug": "juniper" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 3, - "fields": { - "name": "ServerTech", - "slug": "servertech" - } -}, -{ - "model": "dcim.devicetype", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "MX960", - "slug": "mx960", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "EX9214", - "slug": "ex9214", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-24Q", - "slug": "qfx5100-24q", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-48S", - "slug": "qfx5100-48s", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 2, - "model": "CM4148", - "slug": "cm4148", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 3, - "model": "CWG-24VYM415C9", - "slug": "cwg-24vym415c9", - "u_height": 0, - "is_full_depth": false - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 3, - "fields": { - "device_type": 2, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 5, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "Serial" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 1, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 3, - "fields": { - "device_type": 4, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 4, - "fields": { - "device_type": 5, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 5, - "fields": { - "device_type": 5, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 7, - "fields": { - "device_type": 5, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 8, - "fields": { - "device_type": 5, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 9, - "fields": { - "device_type": 5, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 10, - "fields": { - "device_type": 5, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 11, - "fields": { - "device_type": 5, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 12, - "fields": { - "device_type": 5, - "name": "Port 9" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 13, - "fields": { - "device_type": 5, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 15, - "fields": { - "device_type": 5, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 16, - "fields": { - "device_type": 5, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 17, - "fields": { - "device_type": 5, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 18, - "fields": { - "device_type": 5, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 19, - "fields": { - "device_type": 5, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 20, - "fields": { - "device_type": 5, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 21, - "fields": { - "device_type": 5, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 22, - "fields": { - "device_type": 5, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 23, - "fields": { - "device_type": 5, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 24, - "fields": { - "device_type": 5, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 25, - "fields": { - "device_type": 5, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 26, - "fields": { - "device_type": 5, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 27, - "fields": { - "device_type": 5, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 28, - "fields": { - "device_type": 5, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 29, - "fields": { - "device_type": 5, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 30, - "fields": { - "device_type": 5, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 31, - "fields": { - "device_type": 5, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 32, - "fields": { - "device_type": 5, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 33, - "fields": { - "device_type": 5, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 34, - "fields": { - "device_type": 5, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 35, - "fields": { - "device_type": 5, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 36, - "fields": { - "device_type": 5, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 37, - "fields": { - "device_type": 5, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 38, - "fields": { - "device_type": 5, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 39, - "fields": { - "device_type": 5, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 40, - "fields": { - "device_type": 5, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 41, - "fields": { - "device_type": 5, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 42, - "fields": { - "device_type": 5, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 43, - "fields": { - "device_type": 5, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 44, - "fields": { - "device_type": 5, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 45, - "fields": { - "device_type": 5, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 46, - "fields": { - "device_type": 5, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 47, - "fields": { - "device_type": 5, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 48, - "fields": { - "device_type": 5, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 49, - "fields": { - "device_type": 5, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 50, - "fields": { - "device_type": 5, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 51, - "fields": { - "device_type": 5, - "name": "Port 48" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 4, - "fields": { - "device_type": 1, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 7, - "fields": { - "device_type": 2, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 8, - "fields": { - "device_type": 2, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 9, - "fields": { - "device_type": 4, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 13, - "fields": { - "device_type": 4, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "PSU" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 4, - "fields": { - "device_type": 6, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 5, - "fields": { - "device_type": 6, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 6, - "fields": { - "device_type": 6, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 8, - "fields": { - "device_type": 6, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 9, - "fields": { - "device_type": 6, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 10, - "fields": { - "device_type": 6, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 11, - "fields": { - "device_type": 6, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 12, - "fields": { - "device_type": 6, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 13, - "fields": { - "device_type": 6, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 14, - "fields": { - "device_type": 6, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 15, - "fields": { - "device_type": 6, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 16, - "fields": { - "device_type": 6, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 17, - "fields": { - "device_type": 6, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 18, - "fields": { - "device_type": 6, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 19, - "fields": { - "device_type": 6, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 20, - "fields": { - "device_type": 6, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 21, - "fields": { - "device_type": 6, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 22, - "fields": { - "device_type": 6, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 23, - "fields": { - "device_type": 6, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 24, - "fields": { - "device_type": 6, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 25, - "fields": { - "device_type": 6, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 26, - "fields": { - "device_type": 6, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 27, - "fields": { - "device_type": 6, - "name": "AC8" - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 7, - "fields": { - "device_type": 3, - "name": "em0", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 8, - "fields": { - "device_type": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 9, - "fields": { - "device_type": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 10, - "fields": { - "device_type": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 13, - "fields": { - "device_type": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 14, - "fields": { - "device_type": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 15, - "fields": { - "device_type": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 16, - "fields": { - "device_type": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 17, - "fields": { - "device_type": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 18, - "fields": { - "device_type": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 19, - "fields": { - "device_type": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 20, - "fields": { - "device_type": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 21, - "fields": { - "device_type": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 22, - "fields": { - "device_type": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 23, - "fields": { - "device_type": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 24, - "fields": { - "device_type": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 25, - "fields": { - "device_type": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 26, - "fields": { - "device_type": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 27, - "fields": { - "device_type": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 28, - "fields": { - "device_type": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 29, - "fields": { - "device_type": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 30, - "fields": { - "device_type": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 31, - "fields": { - "device_type": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 32, - "fields": { - "device_type": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 33, - "fields": { - "device_type": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 34, - "fields": { - "device_type": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 35, - "fields": { - "device_type": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 36, - "fields": { - "device_type": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 37, - "fields": { - "device_type": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 38, - "fields": { - "device_type": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 138, - "fields": { - "device_type": 4, - "name": "em0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 139, - "fields": { - "device_type": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 140, - "fields": { - "device_type": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 141, - "fields": { - "device_type": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 142, - "fields": { - "device_type": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 143, - "fields": { - "device_type": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 144, - "fields": { - "device_type": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 145, - "fields": { - "device_type": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 146, - "fields": { - "device_type": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 147, - "fields": { - "device_type": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 148, - "fields": { - "device_type": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 149, - "fields": { - "device_type": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 150, - "fields": { - "device_type": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 151, - "fields": { - "device_type": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 152, - "fields": { - "device_type": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 153, - "fields": { - "device_type": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 154, - "fields": { - "device_type": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 155, - "fields": { - "device_type": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 156, - "fields": { - "device_type": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 157, - "fields": { - "device_type": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 158, - "fields": { - "device_type": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 159, - "fields": { - "device_type": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 160, - "fields": { - "device_type": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 161, - "fields": { - "device_type": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 162, - "fields": { - "device_type": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 163, - "fields": { - "device_type": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 164, - "fields": { - "device_type": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 165, - "fields": { - "device_type": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 166, - "fields": { - "device_type": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 167, - "fields": { - "device_type": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 168, - "fields": { - "device_type": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 169, - "fields": { - "device_type": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 170, - "fields": { - "device_type": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 171, - "fields": { - "device_type": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 172, - "fields": { - "device_type": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 173, - "fields": { - "device_type": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 174, - "fields": { - "device_type": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 175, - "fields": { - "device_type": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 176, - "fields": { - "device_type": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 177, - "fields": { - "device_type": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 178, - "fields": { - "device_type": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 179, - "fields": { - "device_type": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 180, - "fields": { - "device_type": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 181, - "fields": { - "device_type": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 182, - "fields": { - "device_type": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 183, - "fields": { - "device_type": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 184, - "fields": { - "device_type": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 185, - "fields": { - "device_type": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 186, - "fields": { - "device_type": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 187, - "fields": { - "device_type": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 188, - "fields": { - "device_type": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 189, - "fields": { - "device_type": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 190, - "fields": { - "device_type": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 191, - "fields": { - "device_type": 5, - "name": "eth0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 192, - "fields": { - "device_type": 6, - "name": "Net", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.devicerole", - "pk": 1, - "fields": { - "name": "Router", - "slug": "router", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 2, - "fields": { - "name": "Spine Switch", - "slug": "spine-switch", - "color": "green" - } -}, -{ - "model": "dcim.devicerole", - "pk": 3, - "fields": { - "name": "Core Switch", - "slug": "core-switch", - "color": "red" - } -}, -{ - "model": "dcim.devicerole", - "pk": 4, - "fields": { - "name": "Leaf Switch", - "slug": "leaf-switch", - "color": "teal" - } -}, -{ - "model": "dcim.devicerole", - "pk": 5, - "fields": { - "name": "OOB Switch", - "slug": "oob-switch", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 6, - "fields": { - "name": "PDU", - "slug": "pdu", - "color": "yellow" - } -}, -{ - "model": "dcim.platform", - "pk": 1, - "fields": { - "name": "Juniper Junos", - "slug": "juniper-junos" - } -}, -{ - "model": "dcim.platform", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.device", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge1", - "serial": "5555555555", - "site": 1, - "rack": 1, - "position": 1, - "face": "front", - "status": true, - "primary_ip4": 1, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core1", - "serial": "", - "site": 1, - "rack": 1, - "position": 17, - "face": "rear", - "status": true, - "primary_ip4": 5, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine1", - "serial": "", - "site": 1, - "rack": 1, - "position": 33, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf1", - "serial": "", - "site": 1, - "rack": 1, - "position": 34, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf2", - "serial": "9823478293748", - "site": 1, - "rack": 2, - "position": 34, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine2", - "serial": "45649818158", - "site": 1, - "rack": 2, - "position": 33, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 7, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge2", - "serial": "7567356345", - "site": 1, - "rack": 2, - "position": 1, - "face": "rear", - "status": true, - "primary_ip4": 3, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 8, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core2", - "serial": "67856734534", - "site": 1, - "rack": 2, - "position": 17, - "face": "rear", - "status": true, - "primary_ip4": 19, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 9, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 5, - "device_role": 5, - "platform": 2, - "name": "test1-oob1", - "serial": "98273942938", - "site": 1, - "rack": 1, - "position": 42, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 11, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu1", - "serial": "", - "site": 1, - "rack": 1, - "position": null, - "face": "", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 12, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu2", - "serial": "", - "site": 1, - "rack": 2, - "position": null, - "face": "", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.consoleport", - "pk": 1, - "fields": { - "device": 1, - "name": "Console (RE0)", - "connected_endpoint": 27, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 2, - "fields": { - "device": 1, - "name": "Console (RE1)", - "connected_endpoint": 38, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 3, - "fields": { - "device": 2, - "name": "Console (RE0)", - "connected_endpoint": 5, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 4, - "fields": { - "device": 2, - "name": "Console (RE1)", - "connected_endpoint": 16, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 5, - "fields": { - "device": 3, - "name": "Console", - "connected_endpoint": 49, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 6, - "fields": { - "device": 4, - "name": "Console", - "connected_endpoint": 48, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 7, - "fields": { - "device": 5, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 8, - "fields": { - "device": 6, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 9, - "fields": { - "device": 7, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 10, - "fields": { - "device": 7, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 11, - "fields": { - "device": 8, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 12, - "fields": { - "device": 8, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 13, - "fields": { - "device": 9, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 15, - "fields": { - "device": 11, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 16, - "fields": { - "device": 12, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 5, - "fields": { - "device": 9, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 6, - "fields": { - "device": 9, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 7, - "fields": { - "device": 9, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 8, - "fields": { - "device": 9, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 9, - "fields": { - "device": 9, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 10, - "fields": { - "device": 9, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 11, - "fields": { - "device": 9, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 12, - "fields": { - "device": 9, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 13, - "fields": { - "device": 9, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 14, - "fields": { - "device": 9, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 15, - "fields": { - "device": 9, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 16, - "fields": { - "device": 9, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 17, - "fields": { - "device": 9, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 18, - "fields": { - "device": 9, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 19, - "fields": { - "device": 9, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 20, - "fields": { - "device": 9, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 21, - "fields": { - "device": 9, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 22, - "fields": { - "device": 9, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 23, - "fields": { - "device": 9, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 24, - "fields": { - "device": 9, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 25, - "fields": { - "device": 9, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 26, - "fields": { - "device": 9, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 27, - "fields": { - "device": 9, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 28, - "fields": { - "device": 9, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 29, - "fields": { - "device": 9, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 30, - "fields": { - "device": 9, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 31, - "fields": { - "device": 9, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 32, - "fields": { - "device": 9, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 33, - "fields": { - "device": 9, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 34, - "fields": { - "device": 9, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 35, - "fields": { - "device": 9, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 36, - "fields": { - "device": 9, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 37, - "fields": { - "device": 9, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 38, - "fields": { - "device": 9, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 39, - "fields": { - "device": 9, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 40, - "fields": { - "device": 9, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 41, - "fields": { - "device": 9, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 42, - "fields": { - "device": 9, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 43, - "fields": { - "device": 9, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 44, - "fields": { - "device": 9, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 45, - "fields": { - "device": 9, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 46, - "fields": { - "device": 9, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 47, - "fields": { - "device": 9, - "name": "Port 48" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 48, - "fields": { - "device": 9, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 49, - "fields": { - "device": 9, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 50, - "fields": { - "device": 9, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 51, - "fields": { - "device": 9, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 52, - "fields": { - "device": 9, - "name": "Port 9" - } -}, -{ - "model": "dcim.powerport", - "pk": 1, - "fields": { - "device": 1, - "name": "PEM0", - "_connected_poweroutlet": 25, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 2, - "fields": { - "device": 1, - "name": "PEM1", - "_connected_poweroutlet": 49, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 3, - "fields": { - "device": 1, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 4, - "fields": { - "device": 1, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 5, - "fields": { - "device": 2, - "name": "PEM0", - "_connected_poweroutlet": 26, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 6, - "fields": { - "device": 2, - "name": "PEM1", - "_connected_poweroutlet": 50, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 7, - "fields": { - "device": 2, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 8, - "fields": { - "device": 2, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 9, - "fields": { - "device": 4, - "name": "PSU0", - "_connected_poweroutlet": 28, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 10, - "fields": { - "device": 4, - "name": "PSU1", - "_connected_poweroutlet": 52, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 11, - "fields": { - "device": 5, - "name": "PSU0", - "_connected_poweroutlet": 56, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 12, - "fields": { - "device": 5, - "name": "PSU1", - "_connected_poweroutlet": 32, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 13, - "fields": { - "device": 3, - "name": "PSU0", - "_connected_poweroutlet": 27, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 14, - "fields": { - "device": 3, - "name": "PSU1", - "_connected_poweroutlet": 51, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 15, - "fields": { - "device": 7, - "name": "PEM0", - "_connected_poweroutlet": 53, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 16, - "fields": { - "device": 7, - "name": "PEM1", - "_connected_poweroutlet": 29, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 17, - "fields": { - "device": 7, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 18, - "fields": { - "device": 7, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 19, - "fields": { - "device": 8, - "name": "PEM0", - "_connected_poweroutlet": 54, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 20, - "fields": { - "device": 8, - "name": "PEM1", - "_connected_poweroutlet": 30, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 21, - "fields": { - "device": 8, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 22, - "fields": { - "device": 8, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 23, - "fields": { - "device": 6, - "name": "PSU0", - "_connected_poweroutlet": 55, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 24, - "fields": { - "device": 6, - "name": "PSU1", - "_connected_poweroutlet": 31, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 25, - "fields": { - "device": 9, - "name": "PSU", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 25, - "fields": { - "device": 11, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 26, - "fields": { - "device": 11, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 27, - "fields": { - "device": 11, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 28, - "fields": { - "device": 11, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 29, - "fields": { - "device": 11, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 30, - "fields": { - "device": 11, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 31, - "fields": { - "device": 11, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 32, - "fields": { - "device": 11, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 33, - "fields": { - "device": 11, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 34, - "fields": { - "device": 11, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 35, - "fields": { - "device": 11, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 36, - "fields": { - "device": 11, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 37, - "fields": { - "device": 11, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 38, - "fields": { - "device": 11, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 39, - "fields": { - "device": 11, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 40, - "fields": { - "device": 11, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 41, - "fields": { - "device": 11, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 42, - "fields": { - "device": 11, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 43, - "fields": { - "device": 11, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 44, - "fields": { - "device": 11, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 45, - "fields": { - "device": 11, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 46, - "fields": { - "device": 11, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 47, - "fields": { - "device": 11, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 48, - "fields": { - "device": 11, - "name": "AC8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 49, - "fields": { - "device": 12, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 50, - "fields": { - "device": 12, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 51, - "fields": { - "device": 12, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 52, - "fields": { - "device": 12, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 53, - "fields": { - "device": 12, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 54, - "fields": { - "device": 12, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 55, - "fields": { - "device": 12, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 56, - "fields": { - "device": 12, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 57, - "fields": { - "device": 12, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 58, - "fields": { - "device": 12, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 59, - "fields": { - "device": 12, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 60, - "fields": { - "device": 12, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 61, - "fields": { - "device": 12, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 62, - "fields": { - "device": 12, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 63, - "fields": { - "device": 12, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 64, - "fields": { - "device": 12, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 65, - "fields": { - "device": 12, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 66, - "fields": { - "device": 12, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 67, - "fields": { - "device": 12, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 68, - "fields": { - "device": 12, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 69, - "fields": { - "device": 12, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 70, - "fields": { - "device": 12, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 71, - "fields": { - "device": 12, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 72, - "fields": { - "device": 12, - "name": "AC8" - } -}, -{ - "model": "dcim.interface", - "pk": 1, - "fields": { - "device": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 2, - "fields": { - "device": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 3, - "fields": { - "device": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 4, - "fields": { - "device": 1, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "TEST" - } -}, -{ - "model": "dcim.interface", - "pk": 5, - "fields": { - "device": 1, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 6, - "fields": { - "device": 1, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 7, - "fields": { - "device": 1, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 8, - "fields": { - "device": 1, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 9, - "fields": { - "device": 1, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 10, - "fields": { - "device": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 11, - "fields": { - "device": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 12, - "fields": { - "device": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 13, - "fields": { - "device": 3, - "name": "em0", - "mac_address": "00-00-00-AA-BB-CC", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 14, - "fields": { - "device": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 15, - "fields": { - "device": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 16, - "fields": { - "device": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 17, - "fields": { - "device": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 18, - "fields": { - "device": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 19, - "fields": { - "device": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 20, - "fields": { - "device": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 21, - "fields": { - "device": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 22, - "fields": { - "device": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 23, - "fields": { - "device": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 24, - "fields": { - "device": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 25, - "fields": { - "device": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 26, - "fields": { - "device": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 27, - "fields": { - "device": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 28, - "fields": { - "device": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 29, - "fields": { - "device": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 30, - "fields": { - "device": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 31, - "fields": { - "device": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 32, - "fields": { - "device": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 33, - "fields": { - "device": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 34, - "fields": { - "device": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 35, - "fields": { - "device": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 36, - "fields": { - "device": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 37, - "fields": { - "device": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 38, - "fields": { - "device": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 39, - "fields": { - "device": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 40, - "fields": { - "device": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 41, - "fields": { - "device": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 42, - "fields": { - "device": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 43, - "fields": { - "device": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 44, - "fields": { - "device": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 45, - "fields": { - "device": 4, - "name": "em0", - "type": 1000, - "mac_address": "ff-ee-dd-33-22-11", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 46, - "fields": { - "device": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 47, - "fields": { - "device": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 48, - "fields": { - "device": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 49, - "fields": { - "device": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 50, - "fields": { - "device": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 51, - "fields": { - "device": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 52, - "fields": { - "device": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 53, - "fields": { - "device": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 54, - "fields": { - "device": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 55, - "fields": { - "device": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 56, - "fields": { - "device": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 57, - "fields": { - "device": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 58, - "fields": { - "device": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 59, - "fields": { - "device": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 60, - "fields": { - "device": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 61, - "fields": { - "device": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 62, - "fields": { - "device": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 63, - "fields": { - "device": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 64, - "fields": { - "device": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 65, - "fields": { - "device": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 66, - "fields": { - "device": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 67, - "fields": { - "device": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 68, - "fields": { - "device": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 69, - "fields": { - "device": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 70, - "fields": { - "device": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 71, - "fields": { - "device": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 72, - "fields": { - "device": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 73, - "fields": { - "device": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 74, - "fields": { - "device": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 75, - "fields": { - "device": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 76, - "fields": { - "device": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 77, - "fields": { - "device": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 78, - "fields": { - "device": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 79, - "fields": { - "device": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 80, - "fields": { - "device": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 81, - "fields": { - "device": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 82, - "fields": { - "device": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 83, - "fields": { - "device": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 84, - "fields": { - "device": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 85, - "fields": { - "device": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 86, - "fields": { - "device": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 87, - "fields": { - "device": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 88, - "fields": { - "device": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 89, - "fields": { - "device": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 90, - "fields": { - "device": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 91, - "fields": { - "device": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 92, - "fields": { - "device": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 93, - "fields": { - "device": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 94, - "fields": { - "device": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 95, - "fields": { - "device": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 96, - "fields": { - "device": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 97, - "fields": { - "device": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 98, - "fields": { - "device": 5, - "name": "em0", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 99, - "fields": { - "device": 5, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 100, - "fields": { - "device": 5, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 101, - "fields": { - "device": 5, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 102, - "fields": { - "device": 5, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 103, - "fields": { - "device": 5, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 104, - "fields": { - "device": 5, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 105, - "fields": { - "device": 5, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 106, - "fields": { - "device": 5, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 107, - "fields": { - "device": 5, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 108, - "fields": { - "device": 5, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 109, - "fields": { - "device": 5, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 110, - "fields": { - "device": 5, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 111, - "fields": { - "device": 5, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 112, - "fields": { - "device": 5, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 113, - "fields": { - "device": 5, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 114, - "fields": { - "device": 5, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 115, - "fields": { - "device": 5, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 116, - "fields": { - "device": 5, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 117, - "fields": { - "device": 5, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 118, - "fields": { - "device": 5, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 119, - "fields": { - "device": 5, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 120, - "fields": { - "device": 5, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 121, - "fields": { - "device": 5, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 122, - "fields": { - "device": 5, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 123, - "fields": { - "device": 5, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 124, - "fields": { - "device": 5, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 125, - "fields": { - "device": 5, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 126, - "fields": { - "device": 5, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 127, - "fields": { - "device": 5, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 128, - "fields": { - "device": 5, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 129, - "fields": { - "device": 5, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 130, - "fields": { - "device": 5, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 131, - "fields": { - "device": 5, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 132, - "fields": { - "device": 5, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 133, - "fields": { - "device": 5, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 134, - "fields": { - "device": 5, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 135, - "fields": { - "device": 5, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 136, - "fields": { - "device": 5, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 137, - "fields": { - "device": 5, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 138, - "fields": { - "device": 5, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 139, - "fields": { - "device": 5, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 140, - "fields": { - "device": 5, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 141, - "fields": { - "device": 5, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 142, - "fields": { - "device": 5, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 143, - "fields": { - "device": 5, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 144, - "fields": { - "device": 5, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 145, - "fields": { - "device": 5, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 146, - "fields": { - "device": 5, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 147, - "fields": { - "device": 5, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 148, - "fields": { - "device": 5, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 149, - "fields": { - "device": 5, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 150, - "fields": { - "device": 5, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 151, - "fields": { - "device": 6, - "name": "em0", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 152, - "fields": { - "device": 6, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 153, - "fields": { - "device": 6, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 154, - "fields": { - "device": 6, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 155, - "fields": { - "device": 6, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 156, - "fields": { - "device": 6, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 157, - "fields": { - "device": 6, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 158, - "fields": { - "device": 6, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 159, - "fields": { - "device": 6, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 160, - "fields": { - "device": 6, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 161, - "fields": { - "device": 6, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 162, - "fields": { - "device": 6, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 163, - "fields": { - "device": 6, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 164, - "fields": { - "device": 6, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 165, - "fields": { - "device": 6, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 166, - "fields": { - "device": 6, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 167, - "fields": { - "device": 6, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 168, - "fields": { - "device": 6, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 169, - "fields": { - "device": 6, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 170, - "fields": { - "device": 6, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 171, - "fields": { - "device": 6, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 172, - "fields": { - "device": 6, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 173, - "fields": { - "device": 6, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 174, - "fields": { - "device": 6, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 175, - "fields": { - "device": 6, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 176, - "fields": { - "device": 6, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 177, - "fields": { - "device": 6, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 178, - "fields": { - "device": 6, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 179, - "fields": { - "device": 6, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 180, - "fields": { - "device": 6, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 181, - "fields": { - "device": 6, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 182, - "fields": { - "device": 6, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 183, - "fields": { - "device": 7, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 184, - "fields": { - "device": 7, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 185, - "fields": { - "device": 7, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 186, - "fields": { - "device": 8, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 187, - "fields": { - "device": 8, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 188, - "fields": { - "device": 8, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 189, - "fields": { - "device": 2, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 190, - "fields": { - "device": 2, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 191, - "fields": { - "device": 2, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 192, - "fields": { - "device": 2, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 193, - "fields": { - "device": 2, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 194, - "fields": { - "device": 2, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 195, - "fields": { - "device": 8, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 196, - "fields": { - "device": 8, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 197, - "fields": { - "device": 8, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 198, - "fields": { - "device": 8, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 199, - "fields": { - "device": 8, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 200, - "fields": { - "device": 8, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 201, - "fields": { - "device": 2, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 202, - "fields": { - "device": 2, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 203, - "fields": { - "device": 2, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 204, - "fields": { - "device": 2, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 205, - "fields": { - "device": 2, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 206, - "fields": { - "device": 2, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 207, - "fields": { - "device": 8, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 208, - "fields": { - "device": 8, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 209, - "fields": { - "device": 8, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 210, - "fields": { - "device": 8, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 211, - "fields": { - "device": 8, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 212, - "fields": { - "device": 8, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 213, - "fields": { - "device": 7, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 214, - "fields": { - "device": 7, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 215, - "fields": { - "device": 7, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 216, - "fields": { - "device": 7, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 217, - "fields": { - "device": 7, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 218, - "fields": { - "device": 7, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 219, - "fields": { - "device": 9, - "name": "eth0", - "type": 1000, - "mac_address": "44-55-66-77-88-99", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 221, - "fields": { - "device": 11, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 222, - "fields": { - "device": 12, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -} -] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 84d83a94c..9041468d8 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -5,7 +5,6 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms.array import SimpleArrayField from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Q from mptt.forms import TreeNodeChoiceField from netaddr import EUI from netaddr.core import AddrFormatError @@ -14,7 +13,8 @@ from timezone_field import TimeZoneFormField from circuits.models import Circuit, Provider from extras.forms import ( - AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm, + LocalConfigContextFilterForm, ) from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN from ipam.models import IPAddress, VLAN @@ -22,9 +22,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, - BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, + BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ConfirmationForm, + CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, SelectWithPK, + SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine from .choices import * @@ -41,7 +41,7 @@ DEVICE_BY_PK_RE = r'{\d+\}' INTERFACE_MODE_HELP_TEXT = """ Access: One untagged VLAN
Tagged: One untagged VLAN and/or one or more tagged VLANs
-Tagged All: Implies all VLANs are available (w/optional untagged VLAN) +Tagged (All): Implies all VLANs are available (w/optional untagged VLAN) """ @@ -216,7 +216,7 @@ class RegionFilterForm(BootstrapMixin, forms.Form): # Sites # -class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = TreeNodeChoiceField( queryset=Region.objects.all(), required=False, @@ -264,7 +264,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class SiteCSVForm(forms.ModelForm): +class SiteCSVForm(CustomFieldModelCSVForm): status = CSVChoiceField( choices=SiteStatusChoices, required=False, @@ -367,6 +367,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): value_field="slug", ) ) + tag = TagFilterField(model) # @@ -460,7 +461,7 @@ class RackRoleCSVForm(forms.ModelForm): # Racks # -class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): group = ChainedModelChoiceField( queryset=RackGroup.objects.all(), chains=( @@ -505,7 +506,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class RackCSVForm(forms.ModelForm): +class RackCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -677,7 +678,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=StaticSelect2() ) comments = CommentField( - widget=SmallTextarea + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -742,6 +744,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # @@ -897,7 +900,7 @@ class ManufacturerCSVForm(forms.ModelForm): # Device types # -class DeviceTypeForm(BootstrapMixin, CustomFieldForm): +class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField( slug_source='model' ) @@ -1020,6 +1023,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -1038,16 +1042,37 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortTemplateCreateForm(ComponentForm): +class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=ConsolePortTypeChoices, + choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) +class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsolePortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) + + class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1060,7 +1085,13 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortTemplateCreateForm(ComponentForm): +class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1070,6 +1101,21 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm): ) +class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsoleServerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) + + class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1082,7 +1128,13 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class PowerPortTemplateCreateForm(ComponentForm): +class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1102,6 +1154,31 @@ class PowerPortTemplateCreateForm(ComponentForm): ) +class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum power draw (watts)" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated power draw (watts)" + ) + + class Meta: + nullable_fields = ('type', 'maximum_draw', 'allocated_draw') + + class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1124,7 +1201,13 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletTemplateCreateForm(ComponentForm): +class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1143,13 +1226,35 @@ class PowerOutletTemplateCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) # Limit power_port choices to current DeviceType - self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( - device_type=self.parent + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') ) + self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( + device_type=device_type + ) + + +class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutletTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect2() + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type', 'feed_leg') class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1165,7 +1270,13 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): } -class InterfaceTemplateCreateForm(ComponentForm): +class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1222,7 +1333,13 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): ) -class FrontPortTemplateCreateForm(ComponentForm): +class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1237,18 +1354,21 @@ class FrontPortTemplateCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') + ) + # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. occupied_port_positions = [ (front_port.rear_port_id, front_port.rear_port_position) - for front_port in self.parent.frontport_templates.all() + for front_port in device_type.frontport_templates.all() ] # Populate rear port choices choices = [] - rear_ports = RearPortTemplate.objects.filter(device_type=self.parent) + rear_ports = RearPortTemplate.objects.filter(device_type=device_type) for rear_port in rear_ports: for i in range(1, rear_port.positions + 1): if (rear_port.pk, i) not in occupied_port_positions: @@ -1279,6 +1399,21 @@ class FrontPortTemplateCreateForm(ComponentForm): } +class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1292,7 +1427,13 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class RearPortTemplateCreateForm(ComponentForm): +class RearPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -1301,13 +1442,28 @@ class RearPortTemplateCreateForm(ComponentForm): widget=StaticSelect2(), ) positions = forms.IntegerField( - min_value=1, - max_value=64, + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, initial=1, help_text='The number of front ports which may be mapped to each rear port' ) +class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RearPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1320,12 +1476,29 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayTemplateCreateForm(ComponentForm): +class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) +# TODO: DeviceBayTemplate has no fields suitable for bulk-editing yet +# class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm): +# pk = forms.ModelMultipleChoiceField( +# queryset=FrontPortTemplate.objects.all(), +# widget=forms.MultipleHiddenInput() +# ) +# +# class Meta: +# nullable_fields = () + + # # Component template import forms # @@ -1516,7 +1689,7 @@ class PlatformCSVForm(forms.ModelForm): # Devices # -class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), widget=APISelect( @@ -1548,6 +1721,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), + required=False, widget=APISelect( api_url="/api/dcim/manufacturers/", filter_for={ @@ -1642,6 +1816,16 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): if instance and instance.cluster is not None: kwargs['initial']['cluster_group'] = instance.cluster.group + if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']: + device_type_id = kwargs['initial']['device_type'] + manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first() + kwargs['initial']['manufacturer'] = manufacturer_id + + if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']: + cluster_id = kwargs['initial']['cluster'] + cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first() + kwargs['initial']['cluster_group'] = cluster_group_id + super().__init__(*args, **kwargs) if self.instance.pk: @@ -1714,7 +1898,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class BaseDeviceCSVForm(forms.ModelForm): +class BaseDeviceCSVForm(CustomFieldModelCSVForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -2096,6 +2280,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -2123,8 +2308,8 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mgmt_only = forms.BooleanField( @@ -2144,6 +2329,7 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort + tag = TagFilterField(model) class ConsolePortForm(BootstrapMixin, forms.ModelForm): @@ -2161,7 +2347,13 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortCreateForm(ComponentForm): +class ConsolePortCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2201,6 +2393,7 @@ class ConsolePortCSVForm(forms.ModelForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort + tag = TagFilterField(model) class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): @@ -2218,7 +2411,13 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortCreateForm(ComponentForm): +class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2293,6 +2492,7 @@ class ConsoleServerPortCSVForm(forms.ModelForm): class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort + tag = TagFilterField(model) class PowerPortForm(BootstrapMixin, forms.ModelForm): @@ -2310,7 +2510,13 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): } -class PowerPortCreateForm(ComponentForm): +class PowerPortCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2360,6 +2566,7 @@ class PowerPortCSVForm(forms.ModelForm): class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet + tag = TagFilterField(model) class PowerOutletForm(BootstrapMixin, forms.ModelForm): @@ -2390,7 +2597,13 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletCreateForm(ComponentForm): +class PowerOutletCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2416,11 +2629,13 @@ class PowerOutletCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Limit power_port choices to those on the parent device - self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) + # Limit power_port queryset to PowerPorts which belong to the parent Device + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) class PowerOutletCSVForm(forms.ModelForm): @@ -2478,8 +2693,12 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + widget=forms.HiddenInput() + ) type = forms.ChoiceField( - choices=PowerOutletTypeChoices, + choices=add_blank_choice(PowerOutletTypeChoices), required=False ) feed_leg = forms.ChoiceField( @@ -2504,7 +2723,9 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): super().__init__(*args, **kwargs) # Limit power_port queryset to PowerPorts which belong to the parent Device - self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj) + if 'device' in self.initial: + device = Device.objects.filter(pk=self.initial['device']).first() + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) class PowerOutletBulkRenameForm(BulkRenameForm): @@ -2528,6 +2749,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface + tag = TagFilterField(model) class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): @@ -2584,14 +2806,19 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): type=InterfaceTypeChoices.TYPE_LAG ) else: - device = self.instance.device self.fields['lag'].queryset = Interface.objects.filter( device__in=[self.instance.device, self.instance.device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG ) -class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): +class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2600,7 +2827,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): widget=StaticSelect2(), ) enabled = forms.BooleanField( - required=False + required=False, + initial=True ) lag = forms.ModelChoiceField( queryset=Interface.objects.all(), @@ -2610,8 +2838,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mac_address = forms.CharField( @@ -2655,21 +2883,16 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): ) def __init__(self, *args, **kwargs): - - # Set interfaces enabled by default - kwargs['initial'] = kwargs.get('initial', {}).copy() - kwargs['initial'].update({'enabled': True}) - super().__init__(*args, **kwargs) - # Limit LAG choices to interfaces belonging to this device (or its VC master) - if self.parent is not None: - self.fields['lag'].queryset = Interface.objects.filter( - device__in=[self.parent, self.parent.get_vc_master()], - type=InterfaceTypeChoices.TYPE_LAG - ) - else: - self.fields['lag'].queryset = Interface.objects.none() + # Limit LAG choices to interfaces which belong to the parent device (or VC master) + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + self.fields['lag'].queryset = Interface.objects.filter( + device__in=[device, device.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG + ) class InterfaceCSVForm(forms.ModelForm): @@ -2716,7 +2939,7 @@ class InterfaceCSVForm(forms.ModelForm): super().__init__(*args, **kwargs) # Limit LAG choices to interfaces belonging to this device (or VC master) - if self.is_bound: + if self.is_bound and 'device' in self.data: try: device = self.fields['device'].to_python(self.data['device']) except forms.ValidationError: @@ -2739,11 +2962,15 @@ class InterfaceCSVForm(forms.ModelForm): return self.cleaned_data['enabled'] -class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + widget=forms.HiddenInput() + ) type = forms.ChoiceField( choices=add_blank_choice(InterfaceTypeChoices), required=False, @@ -2765,8 +2992,8 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mgmt_only = forms.NullBooleanField( @@ -2811,14 +3038,24 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo super().__init__(*args, **kwargs) # Limit LAG choices to interfaces which belong to the parent device (or VC master) - device = self.parent_obj - if device is not None: + if 'device' in self.initial: + device = Device.objects.filter(pk=self.initial['device']).first() self.fields['lag'].queryset = Interface.objects.filter( device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG ) - else: - self.fields['lag'].choices = [] + + def clean(self): + + # Untagged interfaces cannot be assigned tagged VLANs + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: + raise forms.ValidationError({ + 'mode': "An access interface cannot have tagged VLANs assigned." + }) + + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] class InterfaceBulkRenameForm(BulkRenameForm): @@ -2841,6 +3078,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): class FrontPortFilterForm(DeviceComponentFilterForm): model = FrontPort + tag = TagFilterField(model) class FrontPortForm(BootstrapMixin, forms.ModelForm): @@ -2870,7 +3108,13 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm): # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic -class FrontPortCreateForm(ComponentForm): +class FrontPortCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -2890,15 +3134,20 @@ class FrontPortCreateForm(ComponentForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + + # Determine which rear port positions are occupied. These will be excluded from the list of available + # mappings. occupied_port_positions = [ (front_port.rear_port_id, front_port.rear_port_position) - for front_port in self.parent.frontports.all() + for front_port in device.frontports.all() ] # Populate rear port choices choices = [] - rear_ports = RearPort.objects.filter(device=self.parent) + rear_ports = RearPort.objects.filter(device=device) for rear_port in rear_ports: for i in range(1, rear_port.positions + 1): if (rear_port.pk, i) not in occupied_port_positions: @@ -3018,6 +3267,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort + tag = TagFilterField(model) class RearPortForm(BootstrapMixin, forms.ModelForm): @@ -3036,7 +3286,13 @@ class RearPortForm(BootstrapMixin, forms.ModelForm): } -class RearPortCreateForm(ComponentForm): +class RearPortCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -3045,8 +3301,8 @@ class RearPortCreateForm(ComponentForm): widget=StaticSelect2(), ) positions = forms.IntegerField( - min_value=1, - max_value=64, + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, initial=1, help_text='The number of front ports which may be mapped to each rear port' ) @@ -3523,7 +3779,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): required=False ) color = forms.CharField( - max_length=6, + max_length=6, # RGB color code required=False, widget=ColorSelect() ) @@ -3602,7 +3858,7 @@ class CableFilterForm(BootstrapMixin, forms.Form): widget=StaticSelect2() ) color = forms.CharField( - max_length=6, + max_length=6, # RGB color code required=False, widget=ColorSelect() ) @@ -3622,6 +3878,7 @@ class CableFilterForm(BootstrapMixin, forms.Form): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay + tag = TagFilterField(model) class DeviceBayForm(BootstrapMixin, forms.ModelForm): @@ -3639,7 +3896,13 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayCreateForm(ComponentForm): +class DeviceBayCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -3811,6 +4074,42 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): } +class InventoryItemCreateForm(BootstrapMixin, forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) + name_pattern = ExpandableNameField( + label='Name' + ) + manufacturer = forms.ModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/" + ) + ) + part_id = forms.CharField( + max_length=50, + required=False, + label='Part ID' + ) + serial = forms.CharField( + max_length=50, + required=False, + ) + asset_tag = forms.CharField( + max_length=50, + required=False, + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class InventoryItemCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), @@ -3921,6 +4220,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -4107,6 +4407,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # @@ -4219,7 +4520,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): # Power feeds # -class PowerFeedForm(BootstrapMixin, CustomFieldForm): +class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): site = ChainedModelChoiceField( queryset=Site.objects.all(), required=False, @@ -4264,7 +4565,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): self.initial['site'] = self.instance.power_panel.site -class PowerFeedCSVForm(forms.ModelForm): +class PowerFeedCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -4347,7 +4648,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd queryset=PowerFeed.objects.all(), widget=forms.MultipleHiddenInput ) - powerpanel = forms.ModelChoiceField( + power_panel = forms.ModelChoiceField( queryset=PowerPanel.objects.all(), required=False, widget=APISelect( @@ -4397,8 +4698,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd max_utilization = forms.IntegerField( required=False ) - comments = forms.CharField( - required=False + comments = CommentField( + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -4484,3 +4786,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): max_utilization = forms.IntegerField( required=False ) + tag = TagFilterField(model) diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py index 4e76a270f..da544bb7a 100644 --- a/netbox/dcim/migrations/0079_3569_rack_fields.py +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -37,7 +37,7 @@ def rack_status_to_slug(apps, schema_editor): def rack_outer_unit_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') for id, slug in RACK_DIMENSION_CHOICES: - Rack.objects.filter(status=str(id)).update(status=slug) + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) class Migration(migrations.Migration): diff --git a/netbox/dcim/migrations/0092_fix_rack_outer_unit.py b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py new file mode 100644 index 000000000..2a8cbf4e5 --- /dev/null +++ b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py @@ -0,0 +1,27 @@ +from django.db import migrations + +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + + +def rack_outer_unit_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_DIMENSION_CHOICES: + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0091_interface_type_other'), + ] + + operations = [ + # Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed, + # so this can be omitted when squashing in the future. + migrations.RunPython( + code=rack_outer_unit_to_slug + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 1c9c8682d..2a4988ba4 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -405,7 +405,7 @@ class RackElevationHelperMixin: @staticmethod def _draw_device_rear(drawing, device, start, end, text): - rect = drawing.rect(start, end, class_="blocked") + rect = drawing.rect(start, end, class_="slot blocked") rect.set_desc('{} — {} ({}U) {} {}'.format( device.device_role, device.device_type.display_name, device.device_type.u_height, device.asset_tag or '', device.serial or '' @@ -414,7 +414,7 @@ class RackElevationHelperMixin: drawing.add(drawing.text(str(device), insert=text)) @staticmethod - def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_): + def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation): link = drawing.add( drawing.a( href='{}?{}'.format( @@ -424,6 +424,10 @@ class RackElevationHelperMixin: target='_top' ) ) + if reservation: + link.set_desc('{} — {} · {}'.format( + reservation.description, reservation.user, reservation.created + )) link.add(drawing.rect(start, end, class_=class_)) link.add(drawing.text("add device", insert=text, class_='add-device')) @@ -453,12 +457,13 @@ class RackElevationHelperMixin: else: # Draw shallow devices, reservations, or empty units class_ = 'slot' + reservation = reserved_units.get(unit["id"]) if device: class_ += ' occupied' - if unit["id"] in reserved_units: + if reservation: class_ += ' reserved' self._draw_empty( - drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_ + drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation ) unit_cursor += height @@ -483,7 +488,12 @@ class RackElevationHelperMixin: return elevation - def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, unit_width=230, unit_height=20): + def get_elevation_svg( + self, + face=DeviceFaceChoices.FACE_FRONT, + unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT, + unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT + ): """ Return an SVG of the rack elevation @@ -493,7 +503,7 @@ class RackElevationHelperMixin: height of the elevation """ elevation = self.merge_elevations(face) - reserved_units = self.get_reserved_units().keys() + reserved_units = self.get_reserved_units() return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height) @@ -569,7 +579,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin): help_text='Rail-to-rail width' ) u_height = models.PositiveSmallIntegerField( - default=42, + default=RACK_U_HEIGHT_DEFAULT, verbose_name='Height (U)', validators=[MinValueValidator(1), MaxValueValidator(100)] ) @@ -1008,9 +1018,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = [ - 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', - ] clone_fields = [ 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', ] @@ -1859,15 +1866,15 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): ) voltage = models.PositiveSmallIntegerField( validators=[MinValueValidator(1)], - default=120 + default=POWERFEED_VOLTAGE_DEFAULT ) amperage = models.PositiveSmallIntegerField( validators=[MinValueValidator(1)], - default=20 + default=POWERFEED_AMPERAGE_DEFAULT ) max_utilization = models.PositiveSmallIntegerField( validators=[MinValueValidator(1), MaxValueValidator(100)], - default=80, + default=POWERFEED_MAX_UTILIZATION_DEFAULT, help_text="Maximum permissible draw (percentage)" ) available_power = models.PositiveIntegerField( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 68bab8037..431419a7a 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -676,7 +676,7 @@ class Interface(CableTermination, ComponentModel): self.untagged_vlan = None # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) - if self.pk and self.mode is not InterfaceModeChoices.MODE_TAGGED: + if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED: self.tagged_vlans.clear() return super().save(*args, **kwargs) @@ -1004,7 +1004,7 @@ class InventoryItem(ComponentModel): return self.name def get_absolute_url(self): - return self.device.get_absolute_url() + return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk}) def to_csv(self): return ( diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 7e1da41d4..1b3076c6c 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -440,7 +440,7 @@ class ConsoleServerPortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsoleServerPortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') empty_text = "None" @@ -777,6 +777,7 @@ class InterfaceTable(BaseTable): class InterfaceDetailTable(DeviceComponentDetailTable): parent = tables.LinkColumn(order_by=('device', 'virtual_machine')) + name = tables.LinkColumn() class Meta(InterfaceTable.Meta): order_by = ('parent', 'name') diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index d7a946568..29e741560 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -2,6 +2,7 @@ from django.test import TestCase from dcim.forms import * from dcim.models import * +from virtualization.models import Cluster, ClusterGroup, ClusterType def get_id(model, slug): @@ -10,71 +11,108 @@ def get_id(model, slug): class DeviceTestCase(TestCase): - fixtures = ['dcim', 'ipam'] + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + rack = Rack.objects.create(name='Rack 1', site=site) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1 + ) + device_role = DeviceRole.objects.create( + name='Device Role 1', slug='device-role-1', color='ff0000' + ) + Platform.objects.create(name='Platform 1', slug='platform-1') + Device.objects.create( + name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1 + ) + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1') + Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group) def test_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, 'face': DeviceFaceChoices.FACE_FRONT, - 'position': 41, - 'platform': get_id(Platform, 'juniper-junos'), + 'position': 2, + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid(), test.fields['position'].choices) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) def test_racked_device_occupied(self): - test = DeviceForm(data={ + form = DeviceForm(data={ 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, - 'platform': get_id(Platform, 'juniper-junos'), + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertFalse(test.is_valid()) + self.assertFalse(form.is_valid()) + self.assertIn('position', form.errors) def test_non_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', - 'face': '', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, + 'face': None, 'position': None, - 'platform': None, + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) - def test_non_racked_device_with_face(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + def test_non_racked_device_with_face_position(self): + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, 'face': DeviceFaceChoices.FACE_REAR, - 'position': None, + 'position': 10, 'platform': None, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertFalse(form.is_valid()) + self.assertIn('face', form.errors) + self.assertIn('position', form.errors) + + def test_initial_data_population(self): + device_type = DeviceType.objects.first() + cluster = Cluster.objects.first() + test = DeviceForm(initial={ + 'device_type': device_type.pk, + 'device_role': DeviceRole.objects.first().pk, + 'status': DeviceStatusChoices.STATUS_ACTIVE, + 'site': Site.objects.first().pk, + 'cluster': cluster.pk, + }) + + # Check that the initial value for the manufacturer is set automatically when assigning the device type + self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk) + + # Check that the initial value for the cluster group is set automatically when assigning the cluster + self.assertEqual(test.initial['cluster_group'], cluster.group.pk) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 856862a3e..2f157d96d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1,116 +1,133 @@ -import urllib.parse +from decimal import Decimal +import pytz import yaml -from django.test import Client, TestCase +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from netaddr import EUI from dcim.choices import * from dcim.constants import * from dcim.models import * -from utilities.testing import create_test_user +from ipam.models import VLAN +from utilities.testing import StandardTestCases -class RegionTestCase(TestCase): +def create_test_device(name): + """ + Convenience method for creating a Device (e.g. for component testing). + """ + site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1') + manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') + devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer) + devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1') + device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_region', - 'dcim.add_region', - ] - ) - self.client = Client() - self.client.force_login(user) + return device + + +class RegionTestCase(StandardTestCases.Views): + model = Region + + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): # Create three Regions - for i in range(1, 4): - Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save() + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() - def test_region_list(self): + cls.form_data = { + 'name': 'Region X', + 'slug': 'region-x', + 'parent': regions[2].pk, + } - url = reverse('dcim:region_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_region_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Region 4,region-4", "Region 5,region-5", "Region 6,region-6", ) - response = self.client.post(reverse('dcim:region_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(Region.objects.count(), 6) +class SiteTestCase(StandardTestCases.Views): + model = Site + @classmethod + def setUpTestData(cls): -class SiteTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_site', - 'dcim.add_site', - ] + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), ) - self.client = Client() - self.client.force_login(user) - - region = Region(name='Region 1', slug='region-1') - region.save() + for region in regions: + region.save() Site.objects.bulk_create([ - Site(name='Site 1', slug='site-1', region=region), - Site(name='Site 2', slug='site-2', region=region), - Site(name='Site 3', slug='site-3', region=region), + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[0]), + Site(name='Site 3', slug='site-3', region=regions[0]), ]) - def test_site_list(self): - - url = reverse('dcim:site_list') - params = { - "region": Region.objects.first().slug, + cls.form_data = { + 'name': 'Site X', + 'slug': 'site-x', + 'status': SiteStatusChoices.STATUS_PLANNED, + 'region': regions[1].pk, + 'tenant': None, + 'facility': 'Facility X', + 'asn': 65001, + 'time_zone': pytz.UTC, + 'description': 'Site description', + 'physical_address': '742 Evergreen Terrace, Springfield, USA', + 'shipping_address': '742 Evergreen Terrace, Springfield, USA', + 'latitude': Decimal('35.780000'), + 'longitude': Decimal('-78.642000'), + 'contact_name': 'Hank Hill', + 'contact_phone': '123-555-9999', + 'contact_email': 'hank@stricklandpropane.com', + 'comments': 'Test site', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_site(self): - - site = Site.objects.first() - response = self.client.get(site.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_site_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Site 4,site-4", "Site 5,site-5", "Site 6,site-6", ) - response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Site.objects.count(), 6) + cls.bulk_edit_data = { + 'status': SiteStatusChoices.STATUS_PLANNED, + 'region': regions[1].pk, + 'tenant': None, + 'asn': 65009, + 'time_zone': pytz.timezone('US/Eastern'), + 'description': 'New description', + } -class RackGroupTestCase(TestCase): +class RackGroupTestCase(StandardTestCases.Views): + model = RackGroup - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rackgroup', - 'dcim.add_rackgroup', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -121,39 +138,30 @@ class RackGroupTestCase(TestCase): RackGroup(name='Rack Group 3', slug='rack-group-3', site=site), ]) - def test_rackgroup_list(self): + cls.form_data = { + 'name': 'Rack Group X', + 'slug': 'rack-group-x', + 'site': site.pk, + } - url = reverse('dcim:rackgroup_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_rackgroup_import(self): - - csv_data = ( + cls.csv_data = ( "site,name,slug", "Site 1,Rack Group 4,rack-group-4", "Site 1,Rack Group 5,rack-group-5", "Site 1,Rack Group 6,rack-group-6", ) - response = self.client.post(reverse('dcim:rackgroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(RackGroup.objects.count(), 6) +class RackRoleTestCase(StandardTestCases.Views): + model = RackRole + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None -class RackRoleTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rackrole', - 'dcim.add_rackrole', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): RackRole.objects.bulk_create([ RackRole(name='Rack Role 1', slug='rack-role-1'), @@ -161,118 +169,149 @@ class RackRoleTestCase(TestCase): RackRole(name='Rack Role 3', slug='rack-role-3'), ]) - def test_rackrole_list(self): + cls.form_data = { + 'name': 'Rack Role X', + 'slug': 'rack-role-x', + 'color': 'c0c0c0', + 'description': 'New role', + } - url = reverse('dcim:rackrole_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_rackrole_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug,color", "Rack Role 4,rack-role-4,ff0000", "Rack Role 5,rack-role-5,00ff00", "Rack Role 6,rack-role-6,0000ff", ) - response = self.client.post(reverse('dcim:rackrole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(RackRole.objects.count(), 6) +class RackReservationTestCase(StandardTestCases.Views): + model = RackReservation + # Disable inapplicable tests + test_get_object = None + test_create_object = None -class RackReservationTestCase(TestCase): + # TODO: Fix URL name for view + test_import_objects = None - def setUp(self): - user = create_test_user(permissions=['dcim.view_rackreservation']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): - site = Site(name='Site 1', slug='site-1') - site.save() + user2 = User.objects.create_user(username='testuser2') + user3 = User.objects.create_user(username='testuser3') + + site = Site.objects.create(name='Site 1', slug='site-1') rack = Rack(name='Rack 1', site=site) rack.save() RackReservation.objects.bulk_create([ - RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'), - RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'), - RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'), + RackReservation(rack=rack, user=user2, units=[1, 2, 3], description='Reservation 1'), + RackReservation(rack=rack, user=user2, units=[4, 5, 6], description='Reservation 2'), + RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'), ]) - def test_rackreservation_list(self): - - url = reverse('dcim:rackreservation_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class RackTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rack', - 'dcim.add_rack', - ] - ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - Rack.objects.bulk_create([ - Rack(name='Rack 1', site=site), - Rack(name='Rack 2', site=site), - Rack(name='Rack 3', site=site), - ]) - - def test_rack_list(self): - - url = reverse('dcim:rack_list') - params = { - "site": Site.objects.first().slug, + cls.form_data = { + 'rack': rack.pk, + 'units': [10, 11, 12], + 'user': user3.pk, + 'tenant': None, + 'description': 'Rack reservation', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'user': user3.pk, + 'tenant': None, + 'description': 'New description', + } - def test_rack(self): - rack = Rack.objects.first() - response = self.client.get(rack.get_absolute_url()) - self.assertEqual(response.status_code, 200) +class RackTestCase(StandardTestCases.Views): + model = Rack - def test_rack_import(self): + @classmethod + def setUpTestData(cls): - csv_data = ( + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + rackgroups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]) + ) + RackGroup.objects.bulk_create(rackgroups) + + rackroles = ( + RackRole(name='Rack Role 1', slug='rack-role-1'), + RackRole(name='Rack Role 2', slug='rack-role-2'), + ) + RackRole.objects.bulk_create(rackroles) + + Rack.objects.bulk_create(( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[0]), + Rack(name='Rack 3', site=sites[0]), + )) + + cls.form_data = { + 'name': 'Rack X', + 'facility_id': 'Facility X', + 'site': sites[1].pk, + 'group': rackgroups[1].pk, + 'tenant': None, + 'status': RackStatusChoices.STATUS_PLANNED, + 'role': rackroles[1].pk, + 'serial': '123456', + 'asset_tag': 'ABCDEF', + 'type': RackTypeChoices.TYPE_CABINET, + 'width': RackWidthChoices.WIDTH_19IN, + 'u_height': 48, + 'desc_units': False, + 'outer_width': 500, + 'outer_depth': 500, + 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER, + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.csv_data = ( "site,name,width,u_height", "Site 1,Rack 4,19,42", "Site 1,Rack 5,19,42", "Site 1,Rack 6,19,42", ) - response = self.client.post(reverse('dcim:rack_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Rack.objects.count(), 6) + cls.bulk_edit_data = { + 'site': sites[1].pk, + 'group': rackgroups[1].pk, + 'tenant': None, + 'status': RackStatusChoices.STATUS_DEPRECATED, + 'role': rackroles[1].pk, + 'serial': '654321', + 'type': RackTypeChoices.TYPE_4POST, + 'width': RackWidthChoices.WIDTH_23IN, + 'u_height': 49, + 'desc_units': True, + 'outer_width': 30, + 'outer_depth': 30, + 'outer_unit': RackDimensionUnitChoices.UNIT_INCH, + 'comments': 'New comments', + } -class ManufacturerTypeTestCase(TestCase): +class ManufacturerTestCase(StandardTestCases.Views): + model = Manufacturer - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_manufacturer', - 'dcim.add_manufacturer', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): Manufacturer.objects.bulk_create([ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -280,73 +319,59 @@ class ManufacturerTypeTestCase(TestCase): Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), ]) - def test_manufacturer_list(self): + cls.form_data = { + 'name': 'Manufacturer X', + 'slug': 'manufacturer-x', + } - url = reverse('dcim:manufacturer_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_manufacturer_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Manufacturer 4,manufacturer-4", "Manufacturer 5,manufacturer-5", "Manufacturer 6,manufacturer-6", ) - response = self.client.post(reverse('dcim:manufacturer_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(Manufacturer.objects.count(), 6) +class DeviceTypeTestCase(StandardTestCases.Views): + model = DeviceType + @classmethod + def setUpTestData(cls): -class DeviceTypeTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_devicetype']) - self.client = Client() - self.client.force_login(user) - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2') + ) + Manufacturer.objects.bulk_create(manufacturers) DeviceType.objects.bulk_create([ - DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer), - DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer), - DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer), + DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]), + DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturers[0]), + DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]), ]) - def test_devicetype_list(self): - - url = reverse('dcim:devicetype_list') - params = { - "manufacturer": Manufacturer.objects.first().slug, + cls.form_data = { + 'manufacturer': manufacturers[1].pk, + 'model': 'Device Type X', + 'slug': 'device-type-x', + 'part_number': '123ABC', + 'u_height': 2, + 'is_full_depth': True, + 'subdevice_role': '', # CharField + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_devicetype_export(self): - - url = reverse('dcim:devicetype_list') - - response = self.client.get('{}?export'.format(url)) - self.assertEqual(response.status_code, 200) - data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) - self.assertEqual(len(data), 3) - self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1') - self.assertEqual(data[0]['model'], 'Device Type 1') - - def test_devicetype(self): - - devicetype = DeviceType.objects.first() - response = self.client.get(devicetype.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_devicetype_import(self): + cls.bulk_edit_data = { + 'manufacturer': manufacturers[1].pk, + 'u_height': 3, + 'is_full_depth': False, + } + def test_import_objects(self): + """ + Custom import test for YAML-based imports (versus CSV) + """ IMPORT_DATA = """ manufacturer: Generic model: TEST-1000 @@ -420,8 +445,8 @@ device-bays: # Create the manufacturer Manufacturer(name='Generic', slug='generic').save() - # Authenticate as user with necessary permissions - user = create_test_user(username='testuser2', permissions=[ + # Add all required permissions to the test user + self.add_permissions( 'dcim.view_devicetype', 'dcim.add_devicetype', 'dcim.add_consoleporttemplate', @@ -432,15 +457,14 @@ device-bays: 'dcim.add_frontporttemplate', 'dcim.add_rearporttemplate', 'dcim.add_devicebaytemplate', - ]) - self.client.force_login(user) + ) form_data = { 'data': IMPORT_DATA, 'format': 'yaml' } response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) dt = DeviceType.objects.get(model='TEST-1000') @@ -487,18 +511,416 @@ device-bays: db1 = DeviceBayTemplate.objects.first() self.assertEqual(db1.name, 'Device Bay 1') + def test_devicetype_export(self): -class DeviceRoleTestCase(TestCase): + url = reverse('dcim:devicetype_list') + self.add_permissions('dcim.view_devicetype') - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_devicerole', - 'dcim.add_devicerole', - ] + response = self.client.get('{}?export'.format(url)) + self.assertEqual(response.status_code, 200) + data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) + self.assertEqual(len(data), 3) + self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1') + self.assertEqual(data[0]['model'], 'Device Type 1') + + +# +# DeviceType components +# + +class ConsolePortTemplateTestCase(StandardTestCases.Views): + model = ConsolePortTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), ) - self.client = Client() - self.client.force_login(user) + DeviceType.objects.bulk_create(devicetypes) + + ConsolePortTemplate.objects.bulk_create(( + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'), + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'), + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Console Port Template X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Console Port Template [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + +class ConsoleServerPortTemplateTestCase(StandardTestCases.Views): + model = ConsoleServerPortTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + ConsoleServerPortTemplate.objects.bulk_create(( + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'), + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'), + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Console Server Port Template X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Console Server Port Template [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + +class PowerPortTemplateTestCase(StandardTestCases.Views): + model = PowerPortTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + PowerPortTemplate.objects.bulk_create(( + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'), + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'), + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Power Port Template X', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Power Port Template [4-6]', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + cls.bulk_edit_data = { + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + +class PowerOutletTemplateTestCase(StandardTestCases.Views): + model = PowerOutletTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') + + PowerOutletTemplate.objects.bulk_create(( + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 3'), + )) + + powerports = ( + PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'), + ) + PowerPortTemplate.objects.bulk_create(powerports) + + cls.form_data = { + 'device_type': devicetype.pk, + 'name': 'Power Outlet Template X', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[0].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + cls.bulk_create_data = { + 'device_type': devicetype.pk, + 'name_pattern': 'Power Outlet Template [4-6]', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[0].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + cls.bulk_edit_data = { + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + +class InterfaceTemplateTestCase(StandardTestCases.Views): + model = InterfaceTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + InterfaceTemplate.objects.bulk_create(( + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'), + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'), + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Interface Template X', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Interface Template [4-6]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + cls.bulk_edit_data = { + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + +class FrontPortTemplateTestCase(StandardTestCases.Views): + model = FrontPortTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') + + rearports = ( + RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 4'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'), + ) + RearPortTemplate.objects.bulk_create(rearports) + + FrontPortTemplate.objects.bulk_create(( + FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1), + )) + + cls.form_data = { + 'device_type': devicetype.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rearports[3].pk, + 'rear_port_position': 1, + } + + cls.bulk_create_data = { + 'device_type': devicetype.pk, + 'name_pattern': 'Front Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port_set': [ + '{}:1'.format(rp.pk) for rp in rearports[3:6] + ], + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + + +class RearPortTemplateTestCase(StandardTestCases.Views): + model = RearPortTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + RearPortTemplate.objects.bulk_create(( + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'), + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'), + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Rear Port Template X', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 2, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Rear Port Template [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 2, + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + + +class DeviceBayTemplateTestCase(StandardTestCases.Views): + model = DeviceBayTemplate + + # Disable inapplicable views + test_get_object = None + test_list_objects = None + test_create_object = None + test_delete_object = None + test_import_objects = None + test_bulk_edit_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + DeviceBayTemplate.objects.bulk_create(( + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'), + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'), + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Device Bay Template X', + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Device Bay Template [4-6]', + } + + +class DeviceRoleTestCase(StandardTestCases.Views): + model = DeviceRole + + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): DeviceRole.objects.bulk_create([ DeviceRole(name='Device Role 1', slug='device-role-1'), @@ -506,156 +928,156 @@ class DeviceRoleTestCase(TestCase): DeviceRole(name='Device Role 3', slug='device-role-3'), ]) - def test_devicerole_list(self): + cls.form_data = { + 'name': 'Devie Role X', + 'slug': 'device-role-x', + 'color': 'c0c0c0', + 'vm_role': False, + 'description': 'New device role', + } - url = reverse('dcim:devicerole_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_devicerole_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug,color", "Device Role 4,device-role-4,ff0000", "Device Role 5,device-role-5,00ff00", "Device Role 6,device-role-6,0000ff", ) - response = self.client.post(reverse('dcim:devicerole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(DeviceRole.objects.count(), 6) +class PlatformTestCase(StandardTestCases.Views): + model = Platform + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None -class PlatformTestCase(TestCase): + @classmethod + def setUpTestData(cls): - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_platform', - 'dcim.add_platform', - ] - ) - self.client = Client() - self.client.force_login(user) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') Platform.objects.bulk_create([ - Platform(name='Platform 1', slug='platform-1'), - Platform(name='Platform 2', slug='platform-2'), - Platform(name='Platform 3', slug='platform-3'), + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturer), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer), ]) - def test_platform_list(self): + cls.form_data = { + 'name': 'Platform X', + 'slug': 'platform-x', + 'manufacturer': manufacturer.pk, + 'napalm_driver': 'junos', + 'napalm_args': None, + } - url = reverse('dcim:platform_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_platform_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Platform 4,platform-4", "Platform 5,platform-5", "Platform 6,platform-6", ) - response = self.client.post(reverse('dcim:platform_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(Platform.objects.count(), 6) +class DeviceTestCase(StandardTestCases.Views): + model = Device + @classmethod + def setUpTestData(cls): -class DeviceTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_device', - 'dcim.add_device', - ] + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), ) - self.client = Client() - self.client.force_login(user) + Site.objects.bulk_create(sites) - site = Site(name='Site 1', slug='site-1') - site.save() + racks = ( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[1]), + ) + Rack.objects.bulk_create(racks) - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() + devicetypes = ( + DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer), + DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer), + ) + DeviceType.objects.bulk_create(devicetypes) - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() + deviceroles = ( + DeviceRole(name='Device Role 1', slug='device-role-1'), + DeviceRole(name='Device Role 2', slug='device-role-2'), + ) + DeviceRole.objects.bulk_create(deviceroles) + + platforms = ( + Platform(name='Platform 1', slug='platform-1'), + Platform(name='Platform 2', slug='platform-2'), + ) + Platform.objects.bulk_create(platforms) Device.objects.bulk_create([ - Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole), - Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole), - Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 1', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), + Device(name='Device 2', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), + Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), ]) - def test_device_list(self): - - url = reverse('dcim:device_list') - params = { - "device_type_id": DeviceType.objects.first().pk, - "role": DeviceRole.objects.first().slug, + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'device_role': deviceroles[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'name': 'Device X', + 'serial': '123456', + 'asset_tag': 'ABCDEF', + 'site': sites[1].pk, + 'rack': racks[1].pk, + 'position': 1, + 'face': DeviceFaceChoices.FACE_FRONT, + 'status': DeviceStatusChoices.STATUS_PLANNED, + 'primary_ip4': None, + 'primary_ip6': None, + 'cluster': None, + 'virtual_chassis': None, + 'vc_position': None, + 'vc_priority': None, + 'comments': 'A new device', + 'tags': 'Alpha,Bravo,Charlie', + 'local_context_data': None, } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_device(self): - - device = Device.objects.first() - response = self.client.get(device.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_device_import(self): - - csv_data = ( + cls.csv_data = ( "device_role,manufacturer,model_name,status,site,name", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", ) - response = self.client.post(reverse('dcim:device_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Device.objects.count(), 6) + cls.bulk_edit_data = { + 'device_type': devicetypes[1].pk, + 'device_role': deviceroles[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'serial': '123456', + 'status': DeviceStatusChoices.STATUS_DECOMMISSIONING, + } -class ConsolePortTestCase(TestCase): +class ConsolePortTestCase(StandardTestCases.Views): + model = ConsolePort - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_consoleport', - 'dcim.add_consoleport', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable views + test_get_object = None + test_create_object = None + test_bulk_edit_objects = None - site = Site(name='Site 1', slug='site-1') - site.save() + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') ConsolePort.objects.bulk_create([ ConsolePort(device=device, name='Console Port 1'), @@ -663,54 +1085,43 @@ class ConsolePortTestCase(TestCase): ConsolePort(device=device, name='Console Port 3'), ]) - def test_consoleport_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Console Port X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console port', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:consoleport_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Console Port [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console port', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_consoleport_import(self): - - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Console Port 4", "Device 1,Console Port 5", "Device 1,Console Port 6", ) - response = self.client.post(reverse('dcim:consoleport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(ConsolePort.objects.count(), 6) +class ConsoleServerPortTestCase(StandardTestCases.Views): + model = ConsoleServerPort + # Disable inapplicable views + test_get_object = None + test_create_object = None -class ConsoleServerPortTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_consoleserverport', - 'dcim.add_consoleserverport', - ] - ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') ConsoleServerPort.objects.bulk_create([ ConsoleServerPort(device=device, name='Console Server Port 1'), @@ -718,54 +1129,50 @@ class ConsoleServerPortTestCase(TestCase): ConsoleServerPort(device=device, name='Console Server Port 3'), ]) - def test_consoleserverport_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Console Server Port X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console server port', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:consoleserverport_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Console Server Port [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console server port', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'device': device.pk, + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'New description', + } - def test_consoleserverport_import(self): - - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Console Server Port 4", "Device 1,Console Server Port 5", "Device 1,Console Server Port 6", ) - response = self.client.post(reverse('dcim:consoleserverport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(ConsoleServerPort.objects.count(), 6) +class PowerPortTestCase(StandardTestCases.Views): + model = PowerPort + # Disable inapplicable views + test_get_object = None + test_bulk_edit_objects = None + test_create_object = None -class PowerPortTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_powerport', - 'dcim.add_powerport', - ] - ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') PowerPort.objects.bulk_create([ PowerPort(device=device, name='Power Port 1'), @@ -773,231 +1180,257 @@ class PowerPortTestCase(TestCase): PowerPort(device=device, name='Power Port 3'), ]) - def test_powerport_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Power Port X', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + 'description': 'A power port', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:powerport_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Power Port [4-6]]', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + 'description': 'A power port', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_powerport_import(self): - - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Power Port 4", "Device 1,Power Port 5", "Device 1,Power Port 6", ) - response = self.client.post(reverse('dcim:powerport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(PowerPort.objects.count(), 6) +class PowerOutletTestCase(StandardTestCases.Views): + model = PowerOutlet + # Disable inapplicable views + test_get_object = None + test_create_object = None -class PowerOutletTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_poweroutlet', - 'dcim.add_poweroutlet', - ] + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + powerports = ( + PowerPort(device=device, name='Power Port 1'), + PowerPort(device=device, name='Power Port 2'), ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + PowerPort.objects.bulk_create(powerports) PowerOutlet.objects.bulk_create([ - PowerOutlet(device=device, name='Power Outlet 1'), - PowerOutlet(device=device, name='Power Outlet 2'), - PowerOutlet(device=device, name='Power Outlet 3'), + PowerOutlet(device=device, name='Power Outlet 1', power_port=powerports[0]), + PowerOutlet(device=device, name='Power Outlet 2', power_port=powerports[0]), + PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]), ]) - def test_poweroutlet_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Power Outlet X', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'A power outlet', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:poweroutlet_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Power Outlet [4-6]', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'A power outlet', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'device': device.pk, + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'New description', + } - def test_poweroutlet_import(self): - - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Power Outlet 4", "Device 1,Power Outlet 5", "Device 1,Power Outlet 6", ) - response = self.client.post(reverse('dcim:poweroutlet_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(PowerOutlet.objects.count(), 6) +class InterfaceTestCase(StandardTestCases.Views): + model = Interface + # Disable inapplicable views + test_create_object = None -class InterfaceTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_interface', - 'dcim.add_interface', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() - - Interface.objects.bulk_create([ + interfaces = ( Interface(device=device, name='Interface 1'), Interface(device=device, name='Interface 2'), Interface(device=device, name='Interface 3'), - ]) + Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG), + ) + Interface.objects.bulk_create(interfaces) - def test_interface_list(self): + vlans = ( + VLAN(vid=1, name='VLAN1', site=device.site), + VLAN(vid=101, name='VLAN101', site=device.site), + VLAN(vid=102, name='VLAN102', site=device.site), + VLAN(vid=103, name='VLAN103', site=device.site), + ) + VLAN.objects.bulk_create(vlans) - url = reverse('dcim:interface_list') + cls.form_data = { + 'device': device.pk, + 'virtual_machine': None, + 'name': 'Interface X', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'A front port', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Interface [4-6]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'A front port', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } - def test_interface_import(self): + cls.bulk_edit_data = { + 'device': device.pk, + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'New description', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + } - csv_data = ( + cls.csv_data = ( "device,name,type", "Device 1,Interface 4,1000BASE-T (1GE)", "Device 1,Interface 5,1000BASE-T (1GE)", "Device 1,Interface 6,1000BASE-T (1GE)", ) - response = self.client.post(reverse('dcim:interface_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(Interface.objects.count(), 6) +class FrontPortTestCase(StandardTestCases.Views): + model = FrontPort + # Disable inapplicable views + test_get_object = None + test_create_object = None -class FrontPortTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_frontport', - 'dcim.add_frontport', - ] + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + rearports = ( + RearPort(device=device, name='Rear Port 1'), + RearPort(device=device, name='Rear Port 2'), + RearPort(device=device, name='Rear Port 3'), + RearPort(device=device, name='Rear Port 4'), + RearPort(device=device, name='Rear Port 5'), + RearPort(device=device, name='Rear Port 6'), ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() - - rearport1 = RearPort(device=device, name='Rear Port 1') - rearport1.save() - rearport2 = RearPort(device=device, name='Rear Port 2') - rearport2.save() - rearport3 = RearPort(device=device, name='Rear Port 3') - rearport3.save() - - # RearPorts for CSV import test - RearPort(device=device, name='Rear Port 4').save() - RearPort(device=device, name='Rear Port 5').save() - RearPort(device=device, name='Rear Port 6').save() + RearPort.objects.bulk_create(rearports) FrontPort.objects.bulk_create([ - FrontPort(device=device, name='Front Port 1', rear_port=rearport1), - FrontPort(device=device, name='Front Port 2', rear_port=rearport2), - FrontPort(device=device, name='Front Port 3', rear_port=rearport3), + FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]), + FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]), + FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]), ]) - def test_frontport_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rearports[3].pk, + 'rear_port_position': 1, + 'description': 'New description', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:frontport_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Front Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port_set': [ + '{}:1'.format(rp.pk) for rp in rearports[3:6] + ], + 'description': 'New description', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + 'description': 'New description', + } - def test_frontport_import(self): - - csv_data = ( + cls.csv_data = ( "device,name,type,rear_port,rear_port_position", "Device 1,Front Port 4,8P8C,Rear Port 4,1", "Device 1,Front Port 5,8P8C,Rear Port 5,1", "Device 1,Front Port 6,8P8C,Rear Port 6,1", ) - response = self.client.post(reverse('dcim:frontport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(FrontPort.objects.count(), 6) +class RearPortTestCase(StandardTestCases.Views): + model = RearPort + # Disable inapplicable views + test_get_object = None + test_create_object = None -class RearPortTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rearport', - 'dcim.add_rearport', - ] - ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') RearPort.objects.bulk_create([ RearPort(device=device, name='Rear Port 1'), @@ -1005,113 +1438,100 @@ class RearPortTestCase(TestCase): RearPort(device=device, name='Rear Port 3'), ]) - def test_rearport_list(self): + cls.form_data = { + 'device': device.pk, + 'name': 'Rear Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 3, + 'description': 'A rear port', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:rearport_list') + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Rear Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 3, + 'description': 'A rear port', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + 'description': 'New description', + } - def test_rearport_import(self): - - csv_data = ( + cls.csv_data = ( "device,name,type,positions", "Device 1,Rear Port 4,8P8C,1", "Device 1,Rear Port 5,8P8C,1", "Device 1,Rear Port 6,8P8C,1", ) - response = self.client.post(reverse('dcim:rearport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(RearPort.objects.count(), 6) +class DeviceBayTestCase(StandardTestCases.Views): + model = DeviceBay + # Disable inapplicable views + test_get_object = None + test_create_object = None -class DeviceBayTestCase(TestCase): + # TODO + test_bulk_edit_objects = None - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_devicebay', - 'dcim.add_devicebay', - ] - ) - self.client = Client() - self.client.force_login(user) + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - site = Site(name='Site 1', slug='site-1') - site.save() + @classmethod + def setUpTestData(cls): + device1 = create_test_device('Device 1') + device2 = create_test_device('Device 2') - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType( - model='Device Type 1', - manufacturer=manufacturer, - subdevice_role=SubdeviceRoleChoices.ROLE_PARENT - ) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + # Update the DeviceType subdevice role to allow adding DeviceBays + DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT) DeviceBay.objects.bulk_create([ - DeviceBay(device=device, name='Device Bay 1'), - DeviceBay(device=device, name='Device Bay 2'), - DeviceBay(device=device, name='Device Bay 3'), + DeviceBay(device=device1, name='Device Bay 1'), + DeviceBay(device=device1, name='Device Bay 2'), + DeviceBay(device=device1, name='Device Bay 3'), ]) - def test_devicebay_list(self): + cls.form_data = { + 'device': device2.pk, + 'name': 'Device Bay X', + 'description': 'A device bay', + 'tags': 'Alpha,Bravo,Charlie', + } - url = reverse('dcim:devicebay_list') + cls.bulk_create_data = { + 'device': device2.pk, + 'name_pattern': 'Device Bay [4-6]', + 'description': 'A device bay', + 'tags': 'Alpha,Bravo,Charlie', + } - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_devicebay_import(self): - - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Device Bay 4", "Device 1,Device Bay 5", "Device 1,Device Bay 6", ) - response = self.client.post(reverse('dcim:devicebay_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(DeviceBay.objects.count(), 6) +class InventoryItemTestCase(StandardTestCases.Views): + model = InventoryItem + # Disable inapplicable views + test_get_object = None + test_create_object = None -class InventoryItemTestCase(TestCase): + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_inventoryitem', - 'dcim.add_inventoryitem', - ] - ) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') InventoryItem.objects.bulk_create([ InventoryItem(device=device, name='Inventory Item 1'), @@ -1119,126 +1539,135 @@ class InventoryItemTestCase(TestCase): InventoryItem(device=device, name='Inventory Item 3'), ]) - def test_inventoryitem_list(self): - - url = reverse('dcim:inventoryitem_list') - params = { - "device_id": Device.objects.first().pk, + cls.form_data = { + 'device': device.pk, + 'manufacturer': manufacturer.pk, + 'name': 'Inventory Item X', + 'parent': None, + 'discovered': False, + 'part_id': '123456', + 'serial': '123ABC', + 'asset_tag': 'ABC123', + 'description': 'An inventory item', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Inventory Item [4-6]', + 'manufacturer': manufacturer.pk, + 'parent': None, + 'discovered': False, + 'part_id': '123456', + 'serial': '123ABC', + 'description': 'An inventory item', + 'tags': 'Alpha,Bravo,Charlie', + } - def test_inventoryitem_import(self): + cls.bulk_edit_data = { + 'device': device.pk, + 'manufacturer': manufacturer.pk, + 'part_id': '123456', + 'description': 'New description', + } - csv_data = ( + cls.csv_data = ( "device,name", "Device 1,Inventory Item 4", "Device 1,Inventory Item 5", "Device 1,Inventory Item 6", ) - response = self.client.post(reverse('dcim:inventoryitem_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(InventoryItem.objects.count(), 6) +class CableTestCase(StandardTestCases.Views): + model = Cable + # TODO: Creation URL needs termination context + test_create_object = None -class CableTestCase(TestCase): + @classmethod + def setUpTestData(cls): - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_cable', - 'dcim.add_cable', - ] + site = Site.objects.create(name='Site 1', slug='site-1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer) + devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole), ) - self.client = Client() - self.client.force_login(user) + Device.objects.bulk_create(devices) - site = Site(name='Site 1', slug='site-1') - site.save() + interfaces = ( + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + ) + Interface.objects.bulk_create(interfaces) - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() + Cable(termination_a=interfaces[0], termination_b=interfaces[3], type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save() - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device1.save() - device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole) - device2.save() - device3 = Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole) - device3.save() - device4 = Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole) - device4.save() - - iface1 = Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface1.save() - iface2 = Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface2.save() - iface3 = Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface3.save() - iface4 = Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface4.save() - iface5 = Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface5.save() - iface6 = Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED) - iface6.save() - - # Interfaces for CSV import testing - Interface(device=device3, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - Interface(device=device3, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - Interface(device=device3, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - Interface(device=device4, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - Interface(device=device4, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - Interface(device=device4, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() - - Cable(termination_a=iface1, termination_b=iface4, type=CableTypeChoices.TYPE_CAT6).save() - Cable(termination_a=iface2, termination_b=iface5, type=CableTypeChoices.TYPE_CAT6).save() - Cable(termination_a=iface3, termination_b=iface6, type=CableTypeChoices.TYPE_CAT6).save() - - def test_cable_list(self): - - url = reverse('dcim:cable_list') - params = { - "type": CableTypeChoices.TYPE_CAT6, + interface_ct = ContentType.objects.get_for_model(Interface) + cls.form_data = { + # Changing terminations not supported when editing an existing Cable + 'termination_a_type': interface_ct.pk, + 'termination_a_id': interfaces[0].pk, + 'termination_b_type': interface_ct.pk, + 'termination_b_id': interfaces[3].pk, + 'type': CableTypeChoices.TYPE_CAT6, + 'status': CableStatusChoices.STATUS_PLANNED, + 'label': 'Label', + 'color': 'c0c0c0', + 'length': 100, + 'length_unit': CableLengthUnitChoices.UNIT_FOOT, } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_cable(self): - - cable = Cable.objects.first() - response = self.client.get(cable.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_cable_import(self): - - csv_data = ( + cls.csv_data = ( "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name", "Device 3,interface,Interface 1,Device 4,interface,Interface 1", "Device 3,interface,Interface 2,Device 4,interface,Interface 2", "Device 3,interface,Interface 3,Device 4,interface,Interface 3", ) - response = self.client.post(reverse('dcim:cable_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Cable.objects.count(), 6) + cls.bulk_edit_data = { + 'type': CableTypeChoices.TYPE_CAT5E, + 'status': CableStatusChoices.STATUS_CONNECTED, + 'label': 'New label', + 'color': '00ff00', + 'length': 50, + 'length_unit': CableLengthUnitChoices.UNIT_METER, + } -class VirtualChassisTestCase(TestCase): +class VirtualChassisTestCase(StandardTestCases.Views): + model = VirtualChassis - def setUp(self): - user = create_test_user(permissions=['dcim.view_virtualchassis']) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_import_objects = None + test_bulk_edit_objects = None + test_bulk_delete_objects = None + + # TODO: Requires special form handling + test_create_object = None + test_edit_object = None + + @classmethod + def setUpTestData(cls): site = Site.objects.create(name='Site 1', slug='site-1') manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') @@ -1277,9 +1706,110 @@ class VirtualChassisTestCase(TestCase): vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3') Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2) - def test_virtualchassis_list(self): - url = reverse('dcim:virtualchassis_list') +class PowerPanelTestCase(StandardTestCases.Views): + model = PowerPanel - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + # Disable inapplicable tests + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + rackgroups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + ) + RackGroup.objects.bulk_create(rackgroups) + + PowerPanel.objects.bulk_create(( + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'), + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 2'), + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 3'), + )) + + cls.form_data = { + 'site': sites[1].pk, + 'rack_group': rackgroups[1].pk, + 'name': 'Power Panel X', + } + + cls.csv_data = ( + "site,rack_group_name,name", + "Site 1,Rack Group 1,Power Panel 4", + "Site 1,Rack Group 1,Power Panel 5", + "Site 1,Rack Group 1,Power Panel 6", + ) + + +class PowerFeedTestCase(StandardTestCases.Views): + model = PowerFeed + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + + powerpanels = ( + PowerPanel(site=site, name='Power Panel 1'), + PowerPanel(site=site, name='Power Panel 2'), + ) + PowerPanel.objects.bulk_create(powerpanels) + + racks = ( + Rack(site=site, name='Rack 1'), + Rack(site=site, name='Rack 2'), + ) + Rack.objects.bulk_create(racks) + + PowerFeed.objects.bulk_create(( + PowerFeed(name='Power Feed 1', power_panel=powerpanels[0], rack=racks[0]), + PowerFeed(name='Power Feed 2', power_panel=powerpanels[0], rack=racks[0]), + PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]), + )) + + cls.form_data = { + 'name': 'Power Feed X', + 'power_panel': powerpanels[1].pk, + 'rack': racks[1].pk, + 'status': PowerFeedStatusChoices.STATUS_PLANNED, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, + 'supply': PowerFeedSupplyChoices.SUPPLY_DC, + 'phase': PowerFeedPhaseChoices.PHASE_3PHASE, + 'voltage': 100, + 'amperage': 100, + 'max_utilization': 50, + 'comments': 'New comments', + 'tags': 'Alpha,Bravo,Charlie', + + # Connection + 'cable': None, + 'connected_endpoint': None, + 'connection_status': None, + } + + cls.csv_data = ( + "site,panel_name,name,voltage,amperage,max_utilization", + "Site 1,Power Panel 1,Power Feed 4,120,20,80", + "Site 1,Power Panel 1,Power Feed 5,120,20,80", + "Site 1,Power Panel 1,Power Feed 6,120,20,80", + ) + + cls.bulk_edit_data = { + 'power_panel': powerpanels[1].pk, + 'rack': racks[1].pk, + 'status': PowerFeedStatusChoices.STATUS_PLANNED, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, + 'supply': PowerFeedSupplyChoices.SUPPLY_DC, + 'phase': PowerFeedPhaseChoices.PHASE_3PHASE, + 'voltage': 100, + 'amperage': 100, + 'max_utilization': 50, + 'comments': 'New comments', + } diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 956b49bc4..94d1d82ed 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -14,317 +14,328 @@ app_name = 'dcim' urlpatterns = [ # Regions - path(r'regions/', views.RegionListView.as_view(), name='region_list'), - path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'), - path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), - path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), - path(r'regions//edit/', views.RegionEditView.as_view(), name='region_edit'), - path(r'regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), + path('regions/', views.RegionListView.as_view(), name='region_list'), + path('regions/add/', views.RegionCreateView.as_view(), name='region_add'), + path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), + path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), + path('regions//edit/', views.RegionEditView.as_view(), name='region_edit'), + path('regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), # Sites - path(r'sites/', views.SiteListView.as_view(), name='site_list'), - path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'), - path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), - path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), - path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), - path(r'sites//', views.SiteView.as_view(), name='site'), - path(r'sites//edit/', views.SiteEditView.as_view(), name='site_edit'), - path(r'sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), - path(r'sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), - path(r'sites//images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), + path('sites/', views.SiteListView.as_view(), name='site_list'), + path('sites/add/', views.SiteCreateView.as_view(), name='site_add'), + path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), + path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), + path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), + path('sites//', views.SiteView.as_view(), name='site'), + path('sites//edit/', views.SiteEditView.as_view(), name='site_edit'), + path('sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), + path('sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), + path('sites//images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), # Rack groups - path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), - path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), - path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), - path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), - path(r'rack-groups//edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), - path(r'rack-groups//changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), + path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), + path('rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), + path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), + path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), + path('rack-groups//edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), + path('rack-groups//changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), # Rack roles - path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), - path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), - path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), - path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), - path(r'rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), - path(r'rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), + path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), + path('rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), + path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), + path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), + path('rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), + path('rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), # Rack reservations - path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), - path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), - path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), - path(r'rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), - path(r'rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), - path(r'rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), + path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), + path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), + path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), + path('rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), + path('rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), + path('rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), # Racks - path(r'racks/', views.RackListView.as_view(), name='rack_list'), - path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), - path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'), - path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), - path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), - path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), - path(r'racks//', views.RackView.as_view(), name='rack'), - path(r'racks//edit/', views.RackEditView.as_view(), name='rack_edit'), - path(r'racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), - path(r'racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), - path(r'racks//reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), - path(r'racks//images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), + path('racks/', views.RackListView.as_view(), name='rack_list'), + path('rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), + path('racks/add/', views.RackCreateView.as_view(), name='rack_add'), + path('racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), + path('racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), + path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), + path('racks//', views.RackView.as_view(), name='rack'), + path('racks//edit/', views.RackEditView.as_view(), name='rack_edit'), + path('racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), + path('racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), + path('racks//reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), + path('racks//images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), # Manufacturers - path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), - path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), - path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), - path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), - path(r'manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), - path(r'manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), + path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), + path('manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), + path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), + path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), + path('manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), + path('manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), # Device types - path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), - path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), - path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), - path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), - path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), - path(r'device-types//', views.DeviceTypeView.as_view(), name='devicetype'), - path(r'device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), - path(r'device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), - path(r'device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), + path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), + path('device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), + path('device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), + path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), + path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), + path('device-types//', views.DeviceTypeView.as_view(), name='devicetype'), + path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), + path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), + path('device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), # Console port templates - path(r'device-types//console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), - path(r'device-types//console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), - path(r'console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), + path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), + path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'), + path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'), + path('console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), # Console server port templates - path(r'device-types//console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), - path(r'device-types//console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), - path(r'console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), + path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'), + path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'), + path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'), + path('console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), # Power port templates - path(r'device-types//power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), - path(r'device-types//power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), - path(r'power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), + path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'), + path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'), + path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'), + path('power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), # Power outlet templates - path(r'device-types//power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), - path(r'device-types//power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), - path(r'power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), + path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'), + path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'), + path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'), + path('power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), # Interface templates - path(r'device-types//interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), - path(r'device-types//interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), - path(r'device-types//interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), - path(r'interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), + path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'), + path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'), + path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'), + path('interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), # Front port templates - path(r'device-types//front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), - path(r'device-types//front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), - path(r'front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), + path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'), + path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'), + path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'), + path('front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), # Rear port templates - path(r'device-types//rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), - path(r'device-types//rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), - path(r'rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), + path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'), + path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'), + path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'), + path('rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), # Device bay templates - path(r'device-types//device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), - path(r'device-types//device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), - path(r'device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), + path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'), + # path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'), + path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'), + path('device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), # Device roles - path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), - path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), - path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), - path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), - path(r'device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), - path(r'device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), + path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), + path('device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), + path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), + path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), + path('device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), + path('device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), # Platforms - path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'), - path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), - path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), - path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), - path(r'platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), - path(r'platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), + path('platforms/', views.PlatformListView.as_view(), name='platform_list'), + path('platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), + path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), + path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), + path('platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), + path('platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), # Devices - path(r'devices/', views.DeviceListView.as_view(), name='device_list'), - path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'), - path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), - path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), - path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), - path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), - path(r'devices//', views.DeviceView.as_view(), name='device'), - path(r'devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), - path(r'devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), - path(r'devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), - path(r'devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), - path(r'devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), - path(r'devices//status/', views.DeviceStatusView.as_view(), name='device_status'), - path(r'devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), - path(r'devices//config/', views.DeviceConfigView.as_view(), name='device_config'), - path(r'devices//add-secret/', secret_add, name='device_addsecret'), - path(r'devices//services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), - path(r'devices//images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), + path('devices/', views.DeviceListView.as_view(), name='device_list'), + path('devices/add/', views.DeviceCreateView.as_view(), name='device_add'), + path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), + path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), + path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), + path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), + path('devices//', views.DeviceView.as_view(), name='device'), + path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), + path('devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), + path('devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), + path('devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), + path('devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), + path('devices//status/', views.DeviceStatusView.as_view(), name='device_status'), + path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), + path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'), + path('devices//add-secret/', secret_add, name='device_addsecret'), + path('devices//services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), + path('devices//images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), # Console ports - path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), - path(r'devices//console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), - path(r'devices//console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), - path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), - path(r'console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), - path(r'console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), - path(r'console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), - path(r'console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), - path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'), + path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), + path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), + path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'), + # TODO: Bulk edit, rename, disconnect views for ConsolePorts + path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), + path('console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), + path('console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), + path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), + path('console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), # Console server ports - path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), - path(r'devices//console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), - path(r'devices//console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), - path(r'devices//console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), - path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'), - path(r'console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), - path(r'console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), - path(r'console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), - path(r'console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), - path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), - path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), - path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'), + path('console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'), + path('console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + path('console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'), + path('console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), + path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), + path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), + path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), + path('console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), + path('console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), + path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), + path('console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), + path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), # Power ports - path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), - path(r'devices//power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), - path(r'devices//power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), - path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'), - path(r'power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), - path(r'power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), - path(r'power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), - path(r'power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), - path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'), + path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'), + path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), + path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'), + # TODO: Bulk edit, rename, disconnect views for PowerPorts + path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), + path('power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), + path('power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), + path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), + path('power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), # Power outlets - path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), - path(r'devices//power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), - path(r'devices//power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), - path(r'devices//power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), - path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'), - path(r'power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), - path(r'power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), - path(r'power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), - path(r'power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), - path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), - path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), - path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'), + path('power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'), + path('power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + path('power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'), + path('power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), + path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), + path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), + path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), + path('power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), + path('power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), + path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), + path('power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), + path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), # Interfaces - path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), - path(r'devices//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), - path(r'devices//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - path(r'devices//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'), - path(r'interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), - path(r'interfaces//', views.InterfaceView.as_view(), name='interface'), - path(r'interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), - path(r'interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), - path(r'interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), - path(r'interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), - path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), - path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), - path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'), + path('interfaces/', views.InterfaceListView.as_view(), name='interface_list'), + path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path('interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'), + path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), + path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), + path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path('interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), + path('interfaces//', views.InterfaceView.as_view(), name='interface'), + path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), + path('interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), + path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), # Front ports - # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), - path(r'devices//front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), - path(r'devices//front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), - path(r'devices//front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), - path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'), - path(r'front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), - path(r'front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), - path(r'front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), - path(r'front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), - path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), - path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), - path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'), + path('front-ports/', views.FrontPortListView.as_view(), name='frontport_list'), + path('front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), + path('front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'), + path('front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), + path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), + path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), + path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), + path('front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), + path('front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), + path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), + path('front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), + # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # Rear ports - # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), - path(r'devices//rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), - path(r'devices//rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), - path(r'devices//rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), - path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'), - path(r'rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), - path(r'rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), - path(r'rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), - path(r'rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), - path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), - path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), - path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'), + path('rear-ports/', views.RearPortListView.as_view(), name='rearport_list'), + path('rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), + path('rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'), + path('rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), + path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), + path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), + path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), + path('rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), + path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), + path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), + path('rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), + # path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # Device bays - path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), - path(r'devices//bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), - path(r'devices//bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), - path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), - path(r'device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), - path(r'device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), - path(r'device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), - path(r'device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), - path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), - path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), + path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), + path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), + path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), + # TODO: Bulk edit view for DeviceBays + path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), + path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), + path('device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), + path('device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), + path('device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), + path('device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), + path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), # Inventory items - path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), - path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), - path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), - path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), - path(r'inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), - path(r'inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), - path(r'devices//inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), + path('inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), + path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'), + path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), + path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), + # TODO: Bulk rename view for InventoryItems + path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), + path('inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), + path('inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), # Cables - path(r'cables/', views.CableListView.as_view(), name='cable_list'), - path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), - path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), - path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), - path(r'cables//', views.CableView.as_view(), name='cable'), - path(r'cables//edit/', views.CableEditView.as_view(), name='cable_edit'), - path(r'cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), - path(r'cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), + path('cables/', views.CableListView.as_view(), name='cable_list'), + path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), + path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), + path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), + path('cables//', views.CableView.as_view(), name='cable'), + path('cables//edit/', views.CableEditView.as_view(), name='cable_edit'), + path('cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), + path('cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), # Console/power/interface connections (read-only) - path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), - path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), - path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), + path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), + path('power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), + path('interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), # Virtual chassis - path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), - path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), - path(r'virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), - path(r'virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), - path(r'virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), - path(r'virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), - path(r'virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), + path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), + path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), + path('virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), + path('virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), + path('virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), + path('virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), + path('virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), # Power panels - path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), - path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), - path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), - path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), - path(r'power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), - path(r'power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), - path(r'power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), - path(r'power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), + path('power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), + path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), + path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), + path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), + path('power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), + path('power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), + path('power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), + path('power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), # Power feeds - path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), - path(r'power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'), - path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), - path(r'power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), - path(r'power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), - path(r'power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), - path(r'power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), - path(r'power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), - path(r'power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), + path('power-feeds/add/', views.PowerFeedCreateView.as_view(), name='powerfeed_add'), + path('power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), + path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), + path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), + path('power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), + path('power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), + path('power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c83cee8fb..f8d6e2211 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -30,6 +30,7 @@ from utilities.views import ( ) from virtualization.models import VirtualMachine from . import filters, forms, tables +from .choices import DeviceFaceChoices from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -376,16 +377,15 @@ class RackElevationListView(PermissionRequiredMixin, View): page = paginator.page(paginator.num_pages) # Determine rack face - if request.GET.get('face') == '1': - face_id = 1 - else: - face_id = 0 + rack_face = request.GET.get('face', DeviceFaceChoices.FACE_FRONT) + if rack_face not in DeviceFaceChoices.values(): + rack_face = DeviceFaceChoices.FACE_FRONT return render(request, 'dcim/rack_elevation_list.html', { 'paginator': paginator, 'page': page, 'total_count': total_count, - 'face_id': face_id, + 'rack_face': rack_face, 'filter_form': forms.RackElevationFilterForm(request.GET), }) @@ -705,8 +705,6 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = ConsolePortTemplate form = forms.ConsolePortTemplateCreateForm model_form = forms.ConsolePortTemplateForm @@ -719,17 +717,21 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.ConsolePortTemplateForm +class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleporttemplate' + queryset = ConsolePortTemplate.objects.all() + table = tables.ConsolePortTemplateTable + form = forms.ConsolePortTemplateBulkEditForm + + class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleporttemplate' queryset = ConsolePortTemplate.objects.all() - parent_model = DeviceType table = tables.ConsolePortTemplateTable class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = ConsoleServerPortTemplate form = forms.ConsoleServerPortTemplateCreateForm model_form = forms.ConsoleServerPortTemplateForm @@ -742,17 +744,21 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView) model_form = forms.ConsoleServerPortTemplateForm +class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleserverporttemplate' + queryset = ConsoleServerPortTemplate.objects.all() + table = tables.ConsoleServerPortTemplateTable + form = forms.ConsoleServerPortTemplateBulkEditForm + + class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverporttemplate' queryset = ConsoleServerPortTemplate.objects.all() - parent_model = DeviceType table = tables.ConsoleServerPortTemplateTable class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = PowerPortTemplate form = forms.PowerPortTemplateCreateForm model_form = forms.PowerPortTemplateForm @@ -765,17 +771,21 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerPortTemplateForm +class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerporttemplate' + queryset = PowerPortTemplate.objects.all() + table = tables.PowerPortTemplateTable + form = forms.PowerPortTemplateBulkEditForm + + class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerporttemplate' queryset = PowerPortTemplate.objects.all() - parent_model = DeviceType table = tables.PowerPortTemplateTable class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlettemplate' - parent_model = DeviceType - parent_field = 'device_type' model = PowerOutletTemplate form = forms.PowerOutletTemplateCreateForm model_form = forms.PowerOutletTemplateForm @@ -788,17 +798,21 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerOutletTemplateForm +class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_poweroutlettemplate' + queryset = PowerOutletTemplate.objects.all() + table = tables.PowerOutletTemplateTable + form = forms.PowerOutletTemplateBulkEditForm + + class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlettemplate' queryset = PowerOutletTemplate.objects.all() - parent_model = DeviceType table = tables.PowerOutletTemplateTable class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interfacetemplate' - parent_model = DeviceType - parent_field = 'device_type' model = InterfaceTemplate form = forms.InterfaceTemplateCreateForm model_form = forms.InterfaceTemplateForm @@ -814,7 +828,6 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView): class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interfacetemplate' queryset = InterfaceTemplate.objects.all() - parent_model = DeviceType table = tables.InterfaceTemplateTable form = forms.InterfaceTemplateBulkEditForm @@ -822,14 +835,11 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interfacetemplate' queryset = InterfaceTemplate.objects.all() - parent_model = DeviceType table = tables.InterfaceTemplateTable class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_frontporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = FrontPortTemplate form = forms.FrontPortTemplateCreateForm model_form = forms.FrontPortTemplateForm @@ -842,17 +852,21 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.FrontPortTemplateForm +class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_frontporttemplate' + queryset = FrontPortTemplate.objects.all() + table = tables.FrontPortTemplateTable + form = forms.FrontPortTemplateBulkEditForm + + class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontporttemplate' queryset = FrontPortTemplate.objects.all() - parent_model = DeviceType table = tables.FrontPortTemplateTable class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_rearporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = RearPortTemplate form = forms.RearPortTemplateCreateForm model_form = forms.RearPortTemplateForm @@ -865,17 +879,21 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.RearPortTemplateForm +class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_rearporttemplate' + queryset = RearPortTemplate.objects.all() + table = tables.RearPortTemplateTable + form = forms.RearPortTemplateBulkEditForm + + class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearporttemplate' queryset = RearPortTemplate.objects.all() - parent_model = DeviceType table = tables.RearPortTemplateTable class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebaytemplate' - parent_model = DeviceType - parent_field = 'device_type' model = DeviceBayTemplate form = forms.DeviceBayTemplateCreateForm model_form = forms.DeviceBayTemplateForm @@ -888,10 +906,16 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.DeviceBayTemplateForm +# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): +# permission_required = 'dcim.change_devicebaytemplate' +# queryset = DeviceBayTemplate.objects.all() +# table = tables.DeviceBayTemplateTable +# form = forms.DeviceBayTemplateBulkEditForm + + class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebaytemplate' queryset = DeviceBayTemplate.objects.all() - parent_model = DeviceType table = tables.DeviceBayTemplateTable @@ -1205,8 +1229,6 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleport' - parent_model = Device - parent_field = 'device' model = ConsolePort form = forms.ConsolePortCreateForm model_form = forms.ConsolePortForm @@ -1234,8 +1256,8 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleport' queryset = ConsolePort.objects.all() - parent_model = Device table = tables.ConsolePortTable + default_return_url = 'dcim:consoleport_list' # @@ -1253,8 +1275,6 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverport' - parent_model = Device - parent_field = 'device' model = ConsoleServerPort form = forms.ConsoleServerPortCreateForm model_form = forms.ConsoleServerPortForm @@ -1282,7 +1302,6 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_consoleserverport' queryset = ConsoleServerPort.objects.all() - parent_model = Device table = tables.ConsoleServerPortTable form = forms.ConsoleServerPortBulkEditForm @@ -1302,8 +1321,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverport' queryset = ConsoleServerPort.objects.all() - parent_model = Device table = tables.ConsoleServerPortTable + default_return_url = 'dcim:consoleserverport_list' # @@ -1321,8 +1340,6 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerport' - parent_model = Device - parent_field = 'device' model = PowerPort form = forms.PowerPortCreateForm model_form = forms.PowerPortForm @@ -1350,8 +1367,8 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerport' queryset = PowerPort.objects.all() - parent_model = Device table = tables.PowerPortTable + default_return_url = 'dcim:powerport_list' # @@ -1369,8 +1386,6 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlet' - parent_model = Device - parent_field = 'device' model = PowerOutlet form = forms.PowerOutletCreateForm model_form = forms.PowerOutletForm @@ -1398,7 +1413,6 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_poweroutlet' queryset = PowerOutlet.objects.all() - parent_model = Device table = tables.PowerOutletTable form = forms.PowerOutletBulkEditForm @@ -1418,8 +1432,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView) class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlet' queryset = PowerOutlet.objects.all() - parent_model = Device table = tables.PowerOutletTable + default_return_url = 'dcim:poweroutlet_list' # @@ -1473,8 +1487,6 @@ class InterfaceView(PermissionRequiredMixin, View): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interface' - parent_model = Device - parent_field = 'device' model = Interface form = forms.InterfaceCreateForm model_form = forms.InterfaceForm @@ -1503,7 +1515,6 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' queryset = Interface.objects.all() - parent_model = Device table = tables.InterfaceTable form = forms.InterfaceBulkEditForm @@ -1523,8 +1534,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interface' queryset = Interface.objects.all() - parent_model = Device table = tables.InterfaceTable + default_return_url = 'dcim:interface_list' # @@ -1542,8 +1553,6 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_frontport' - parent_model = Device - parent_field = 'device' model = FrontPort form = forms.FrontPortCreateForm model_form = forms.FrontPortForm @@ -1571,7 +1580,6 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_frontport' queryset = FrontPort.objects.all() - parent_model = Device table = tables.FrontPortTable form = forms.FrontPortBulkEditForm @@ -1591,8 +1599,8 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontport' queryset = FrontPort.objects.all() - parent_model = Device table = tables.FrontPortTable + default_return_url = 'dcim:frontport_list' # @@ -1610,8 +1618,6 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_rearport' - parent_model = Device - parent_field = 'device' model = RearPort form = forms.RearPortCreateForm model_form = forms.RearPortForm @@ -1639,7 +1645,6 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rearport' queryset = RearPort.objects.all() - parent_model = Device table = tables.RearPortTable form = forms.RearPortBulkEditForm @@ -1659,8 +1664,8 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearport' queryset = RearPort.objects.all() - parent_model = Device table = tables.RearPortTable + default_return_url = 'dcim:rearport_list' # @@ -1680,8 +1685,6 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebay' - parent_model = Device - parent_field = 'device' model = DeviceBay form = forms.DeviceBayCreateForm model_form = forms.DeviceBayForm @@ -1784,8 +1787,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebay' queryset = DeviceBay.objects.all() - parent_model = Device table = tables.DeviceBayTable + default_return_url = 'dcim:devicebay_list' # @@ -2156,13 +2159,13 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView): model = InventoryItem model_form = forms.InventoryItemForm - def alter_obj(self, obj, request, url_args, url_kwargs): - if 'device' in url_kwargs: - obj.device = get_object_or_404(Device, pk=url_kwargs['device']) - return obj - def get_return_url(self, request, obj): - return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) +class InventoryItemCreateView(PermissionRequiredMixin, ComponentCreateView): + permission_required = 'dcim.add_inventoryitem' + model = InventoryItem + form = forms.InventoryItemCreateForm + model_form = forms.InventoryItemForm + template_name = 'dcim/device_component_add.html' class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 0e27a8ee5..58433df25 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -20,6 +20,8 @@ from utilities.api import ( ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField, ValidatedModelSerializer, ) +from virtualization.api.nested_serializers import NestedClusterGroupSerializer, NestedClusterSerializer +from virtualization.models import Cluster, ClusterGroup from .nested_serializers import * @@ -161,6 +163,18 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + cluster_groups = SerializedPKRelatedField( + queryset=ClusterGroup.objects.all(), + serializer=NestedClusterGroupSerializer, + required=False, + many=True + ) + clusters = SerializedPKRelatedField( + queryset=Cluster.objects.all(), + serializer=NestedClusterSerializer, + required=False, + many=True + ) tenant_groups = SerializedPKRelatedField( queryset=TenantGroup.objects.all(), serializer=NestedTenantGroupSerializer, @@ -184,7 +198,7 @@ class ConfigContextSerializer(ValidatedModelSerializer): model = ConfigContext fields = [ 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', - 'tenant_groups', 'tenants', 'tags', 'data', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ] diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 50a54d3fe..d699cd22e 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -15,34 +15,34 @@ router = routers.DefaultRouter() router.APIRootView = ExtrasRootView # Field choices -router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') # Custom field choices -router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') +router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') # Graphs -router.register(r'graphs', views.GraphViewSet) +router.register('graphs', views.GraphViewSet) # Export templates -router.register(r'export-templates', views.ExportTemplateViewSet) +router.register('export-templates', views.ExportTemplateViewSet) # Tags -router.register(r'tags', views.TagViewSet) +router.register('tags', views.TagViewSet) # Image attachments -router.register(r'image-attachments', views.ImageAttachmentViewSet) +router.register('image-attachments', views.ImageAttachmentViewSet) # Config contexts -router.register(r'config-contexts', views.ConfigContextViewSet) +router.register('config-contexts', views.ConfigContextViewSet) # Reports -router.register(r'reports', views.ReportViewSet, basename='report') +router.register('reports', views.ReportViewSet, basename='report') # Scripts -router.register(r'scripts', views.ScriptViewSet, basename='script') +router.register('scripts', views.ScriptViewSet, basename='script') # Change logging -router.register(r'object-changes', views.ObjectChangeViewSet) +router.register('object-changes', views.ObjectChangeViewSet) app_name = 'extras-api' urlpatterns = router.urls diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 8a0d32b33..dcd4f3ede 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,6 +4,7 @@ from django.db.models import Q from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup +from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag @@ -170,6 +171,22 @@ class ConfigContextFilterSet(django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) + cluster_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups', + queryset=ClusterGroup.objects.all(), + label='Cluster group', + ) + cluster_group = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups__slug', + queryset=ClusterGroup.objects.all(), + to_field_name='slug', + label='Cluster group (slug)', + ) + cluster_id = django_filters.ModelMultipleChoiceFilter( + field_name='clusters', + queryset=Cluster.objects.all(), + label='Cluster', + ) tenant_group_id = django_filters.ModelMultipleChoiceFilter( field_name='tenant_groups', queryset=TenantGroup.objects.all(), diff --git a/netbox/extras/fixtures/extras.json b/netbox/extras/fixtures/extras.json deleted file mode 100644 index 83b947cb2..000000000 --- a/netbox/extras/fixtures/extras.json +++ /dev/null @@ -1,35 +0,0 @@ -[ -{ - "model": "extras.graph", - "pk": 1, - "fields": { - "type": 300, - "weight": 1000, - "name": "Site Test Graph", - "source": "http://localhost/na.png", - "link": "" - } -}, -{ - "model": "extras.graph", - "pk": 2, - "fields": { - "type": 200, - "weight": 1000, - "name": "Provider Test Graph", - "source": "http://localhost/provider_graph.png", - "link": "" - } -}, -{ - "model": "extras.graph", - "pk": 3, - "fields": { - "type": 100, - "weight": 1000, - "name": "Interface Test Graph", - "source": "http://localhost/interface_graph.png", - "link": "" - } -} -] \ No newline at end of file diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index edde6c6c5..8c9113d39 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -1,18 +1,16 @@ -from collections import OrderedDict - from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist from taggit.forms import TagField from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, - SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, + CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2, + BOOLEAN_WITH_BLANK_CHOICES, ) +from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag @@ -21,102 +19,41 @@ from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachmen # Custom fields # -def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): - """ - Retrieve all CustomFields applicable to the given ContentType - """ - field_dict = OrderedDict() - custom_fields = CustomField.objects.filter(obj_type=content_type) - if filterable_only: - custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) - - for cf in custom_fields: - field_name = 'cf_{}'.format(str(cf.name)) - initial = cf.default if not bulk_edit else None - - # Integer - if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: - field = forms.IntegerField(required=cf.required, initial=initial) - - # Boolean - elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN: - choices = ( - (None, '---------'), - (1, 'True'), - (0, 'False'), - ) - if initial is not None and initial.lower() in ['true', 'yes', '1']: - initial = 1 - elif initial is not None and initial.lower() in ['false', 'no', '0']: - initial = 0 - else: - initial = None - field = forms.NullBooleanField( - required=cf.required, initial=initial, widget=StaticSelect2(choices=choices) - ) - - # Date - elif cf.type == CustomFieldTypeChoices.TYPE_DATE: - field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker()) - - # Select - elif cf.type == CustomFieldTypeChoices.TYPE_SELECT: - choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] - if not cf.required or bulk_edit or filterable_only: - choices = [(None, '---------')] + choices - # Check for a default choice - default_choice = None - if initial: - try: - default_choice = cf.choices.get(value=initial).pk - except ObjectDoesNotExist: - pass - field = forms.TypedChoiceField( - choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() - ) - - # URL - elif cf.type == CustomFieldTypeChoices.TYPE_URL: - field = LaxURLField(required=cf.required, initial=initial) - - # Text - else: - field = forms.CharField(max_length=255, required=cf.required, initial=initial) - - field.model = cf - field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize() - if cf.description: - field.help_text = cf.description - - field_dict[field_name] = field - - return field_dict - - -class CustomFieldForm(forms.ModelForm): +class CustomFieldModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): - self.custom_fields = [] self.obj_type = ContentType.objects.get_for_model(self._meta.model) + self.custom_fields = [] + self.custom_field_values = {} super().__init__(*args, **kwargs) - # Add all applicable CustomFields to the form - custom_fields = [] - for name, field in get_custom_fields_for_model(self.obj_type).items(): - self.fields[name] = field - custom_fields.append(name) - self.custom_fields = custom_fields + self._append_customfield_fields() - # If editing an existing object, initialize values for all custom fields + def _append_customfield_fields(self): + """ + Append form fields for all CustomFields assigned to this model. + """ + # Retrieve initial CustomField values for the instance if self.instance.pk: - existing_values = CustomFieldValue.objects.filter( + for cfv in CustomFieldValue.objects.filter( obj_type=self.obj_type, obj_id=self.instance.pk - ).prefetch_related('field') - for cfv in existing_values: - self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value + ).prefetch_related('field'): + self.custom_field_values[cfv.field.name] = cfv.serialized_value + + # Append form fields; assign initial values if modifying and existing object + for cf in CustomField.objects.filter(obj_type=self.obj_type): + field_name = 'cf_{}'.format(cf.name) + if self.instance.pk: + self.fields[field_name] = cf.to_form_field(set_initial=False) + self.fields[field_name].initial = self.custom_field_values.get(cf.name) + else: + self.fields[field_name] = cf.to_form_field() + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) def _save_custom_fields(self): @@ -151,6 +88,19 @@ class CustomFieldForm(forms.ModelForm): return obj +class CustomFieldModelCSVForm(CustomFieldModelForm): + + def _append_customfield_fields(self): + + # Append form fields + for cf in CustomField.objects.filter(obj_type=self.obj_type): + field_name = 'cf_{}'.format(cf.name) + self.fields[field_name] = cf.to_form_field(for_csv_import=True) + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) + + class CustomFieldBulkEditForm(BulkEditForm): def __init__(self, *args, **kwargs): @@ -160,15 +110,14 @@ class CustomFieldBulkEditForm(BulkEditForm): self.obj_type = ContentType.objects.get_for_model(self.model) # Add all applicable CustomFields to the form - custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items() - for name, field in custom_fields: + custom_fields = CustomField.objects.filter(obj_type=self.obj_type) + for cf in custom_fields: # Annotate non-required custom fields as nullable - if not field.required: - self.nullable_fields.append(name) - field.required = False - self.fields[name] = field + if not cf.required: + self.nullable_fields.append(cf.name) + self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) # Annotate this as a custom field - self.custom_fields.append(name) + self.custom_fields.append(cf.name) class CustomFieldFilterForm(forms.Form): @@ -180,10 +129,12 @@ class CustomFieldFilterForm(forms.Form): super().__init__(*args, **kwargs) # Add all applicable CustomFields to the form - custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items() - for name, field in custom_fields: - field.required = False - self.fields[name] = field + custom_fields = CustomField.objects.filter(obj_type=self.obj_type).exclude( + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ) + for cf in custom_fields: + field_name = 'cf_{}'.format(cf.name) + self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) # @@ -254,8 +205,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConfigContext fields = [ - 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', - 'tenants', 'tags', 'data', + 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups', + 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ] widgets = { 'regions': APISelectMultiple( @@ -270,6 +221,12 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): 'platforms': APISelectMultiple( api_url="/api/dcim/platforms/" ), + 'cluster_groups': APISelectMultiple( + api_url="/api/virtualization/cluster-groups/" + ), + 'clusters': APISelectMultiple( + api_url="/api/virtualization/clusters/" + ), 'tenant_groups': APISelectMultiple( api_url="/api/tenancy/tenant-groups/" ), @@ -340,6 +297,21 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): value_field="slug", ) ) + cluster_group = FilterChoiceField( + queryset=ClusterGroup.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/virtualization/cluster-groups/", + value_field="slug", + ) + ) + cluster_id = FilterChoiceField( + queryset=Cluster.objects.all(), + label='Cluster', + widget=APISelectMultiple( + api_url="/api/virtualization/clusters/", + ) + ) tenant_group = FilterChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', diff --git a/netbox/extras/migrations/0037_configcontexts_clusters.py b/netbox/extras/migrations/0037_configcontexts_clusters.py new file mode 100644 index 000000000..201aed94a --- /dev/null +++ b/netbox/extras/migrations/0037_configcontexts_clusters.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.8 on 2020-01-17 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0013_deterministic_ordering'), + ('extras', '0036_contenttype_filters_to_q_objects'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='cluster_groups', + field=models.ManyToManyField(blank=True, related_name='_configcontext_cluster_groups_+', to='virtualization.ClusterGroup'), + ), + migrations.AddField( + model_name='configcontext', + name='clusters', + field=models.ManyToManyField(blank=True, related_name='_configcontext_clusters_+', to='virtualization.Cluster'), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index a03494bb2..5d175d172 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,6 +1,7 @@ from collections import OrderedDict from datetime import date +from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -14,6 +15,7 @@ from django.utils.text import slugify from taggit.models import TagBase, GenericTaggedItemBase from utilities.fields import ColorField +from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * @@ -280,6 +282,75 @@ class CustomField(models.Model): return self.choices.get(pk=int(serialized_value)) return serialized_value + def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False): + """ + Return a form field suitable for setting a CustomField's value for an object. + + set_initial: Set initial date for the field. This should be False when generating a field for bulk editing. + enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing. + for_csv_import: Return a form field suitable for bulk import of objects in CSV format. + """ + initial = self.default if set_initial else None + required = self.required if enforce_required else False + + # Integer + if self.type == CustomFieldTypeChoices.TYPE_INTEGER: + field = forms.IntegerField(required=required, initial=initial) + + # Boolean + elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: + choices = ( + (None, '---------'), + (1, 'True'), + (0, 'False'), + ) + if initial is not None and initial.lower() in ['true', 'yes', '1']: + initial = 1 + elif initial is not None and initial.lower() in ['false', 'no', '0']: + initial = 0 + else: + initial = None + field = forms.NullBooleanField( + required=required, initial=initial, widget=StaticSelect2(choices=choices) + ) + + # Date + elif self.type == CustomFieldTypeChoices.TYPE_DATE: + field = forms.DateField(required=required, initial=initial, widget=DatePicker()) + + # Select + elif self.type == CustomFieldTypeChoices.TYPE_SELECT: + choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()] + + if not required: + choices = add_blank_choice(choices) + + # Set the initial value to the PK of the default choice, if any + if set_initial: + default_choice = self.choices.filter(value=self.default).first() + if default_choice: + initial = default_choice.pk + + field_class = CSVChoiceField if for_csv_import else forms.ChoiceField + field = field_class( + choices=choices, required=required, initial=initial, widget=StaticSelect2() + ) + + # URL + elif self.type == CustomFieldTypeChoices.TYPE_URL: + field = LaxURLField(required=required, initial=initial) + + # Text + else: + field = forms.CharField(max_length=255, required=required, initial=initial) + + field.model = self + field.label = self.label if self.label else self.name.replace('_', ' ').capitalize() + if self.description: + field.help_text = self.description + + return field + class CustomFieldValue(models.Model): field = models.ForeignKey( @@ -694,6 +765,16 @@ class ConfigContext(models.Model): related_name='+', blank=True ) + cluster_groups = models.ManyToManyField( + to='virtualization.ClusterGroup', + related_name='+', + blank=True + ) + clusters = models.ManyToManyField( + to='virtualization.Cluster', + related_name='+', + blank=True + ) tenant_groups = models.ManyToManyField( to='tenancy.TenantGroup', related_name='+', diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 22ab489bd..812c66714 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -29,6 +29,10 @@ class ConfigContextQuerySet(QuerySet): # `device_role` for Device; `role` for VirtualMachine role = getattr(obj, 'device_role', None) or obj.role + # Virtualization cluster for VirtualMachine + cluster = getattr(obj, 'cluster', None) + cluster_group = getattr(cluster, 'group', None) + # Get the group of the assigned tenant, if any tenant_group = obj.tenant.group if obj.tenant else None @@ -44,6 +48,8 @@ class ConfigContextQuerySet(QuerySet): Q(sites=obj.site) | Q(sites=None), Q(roles=role) | Q(roles=None), Q(platforms=obj.platform) | Q(platforms=None), + Q(cluster_groups=cluster_group) | Q(cluster_groups=None), + Q(clusters=cluster) | Q(clusters=None), Q(tenant_groups=tenant_group) | Q(tenant_groups=None), Q(tenants=obj.tenant) | Q(tenants=None), Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None), diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index bd7e864e1..6567fe707 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -53,14 +53,15 @@ class ScriptVariable: # Initialize field attributes if not hasattr(self, 'field_attrs'): self.field_attrs = {} - if description: - self.field_attrs['help_text'] = description if label: self.field_attrs['label'] = label + if description: + self.field_attrs['help_text'] = description if default: self.field_attrs['initial'] = default - if required: - self.field_attrs['required'] = True + self.field_attrs['required'] = required + + # Initialize the list of optional validators if none have already been defined if 'validators' not in self.field_attrs: self.field_attrs['validators'] = [] diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 192958840..a6e2bfcec 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,14 +1,15 @@ from datetime import date from django.contrib.contenttypes.models import ContentType -from django.test import TestCase +from django.test import Client, TestCase from django.urls import reverse from rest_framework import status +from dcim.forms import SiteCSVForm from dcim.models import Site from extras.choices import * from extras.models import CustomField, CustomFieldValue, CustomFieldChoice -from utilities.testing import APITestCase +from utilities.testing import APITestCase, create_test_user from virtualization.models import VirtualMachine @@ -364,3 +365,113 @@ class CustomFieldChoiceAPITest(APITestCase): self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value]) self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value]) self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value]) + + +class CustomFieldImportTest(TestCase): + + def setUp(self): + + user = create_test_user( + permissions=[ + 'dcim.view_site', + 'dcim.add_site', + ] + ) + self.client = Client() + self.client.force_login(user) + + @classmethod + def setUpTestData(cls): + + custom_fields = ( + CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT), + CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER), + CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN), + CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE), + CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL), + CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT), + ) + for cf in custom_fields: + cf.save() + cf.obj_type.set([ContentType.objects.get_for_model(Site)]) + + CustomFieldChoice.objects.bulk_create(( + CustomFieldChoice(field=custom_fields[5], value='Choice A'), + CustomFieldChoice(field=custom_fields[5], value='Choice B'), + CustomFieldChoice(field=custom_fields[5], value='Choice C'), + )) + + def test_import(self): + """ + Import a Site in CSV format, including a value for each CustomField. + """ + data = ( + ('name', 'slug', 'cf_text', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_select'), + ('Site 1', 'site-1', 'ABC', '123', 'True', '2020-01-01', 'http://example.com/1', 'Choice A'), + ('Site 2', 'site-2', 'DEF', '456', 'False', '2020-01-02', 'http://example.com/2', 'Choice B'), + ('Site 3', 'site-3', '', '', '', '', '', ''), + ) + csv_data = '\n'.join(','.join(row) for row in data) + + response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data}) + self.assertEqual(response.status_code, 200) + + # Validate data for site 1 + custom_field_values = { + cf.name: value for cf, value in Site.objects.get(name='Site 1').get_custom_fields().items() + } + self.assertEqual(len(custom_field_values), 6) + self.assertEqual(custom_field_values['text'], 'ABC') + self.assertEqual(custom_field_values['integer'], 123) + self.assertEqual(custom_field_values['boolean'], True) + self.assertEqual(custom_field_values['date'], date(2020, 1, 1)) + self.assertEqual(custom_field_values['url'], 'http://example.com/1') + self.assertEqual(custom_field_values['select'].value, 'Choice A') + + # Validate data for site 2 + custom_field_values = { + cf.name: value for cf, value in Site.objects.get(name='Site 2').get_custom_fields().items() + } + self.assertEqual(len(custom_field_values), 6) + self.assertEqual(custom_field_values['text'], 'DEF') + self.assertEqual(custom_field_values['integer'], 456) + self.assertEqual(custom_field_values['boolean'], False) + self.assertEqual(custom_field_values['date'], date(2020, 1, 2)) + self.assertEqual(custom_field_values['url'], 'http://example.com/2') + self.assertEqual(custom_field_values['select'].value, 'Choice B') + + # No CustomFieldValues should be created for site 3 + obj_type = ContentType.objects.get_for_model(Site) + site3 = Site.objects.get(name='Site 3') + self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists()) + self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check + + def test_import_missing_required(self): + """ + Attempt to import an object missing a required custom field. + """ + # Set one of our CustomFields to required + CustomField.objects.filter(name='text').update(required=True) + + form_data = { + 'name': 'Site 1', + 'slug': 'site-1', + } + + form = SiteCSVForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('cf_text', form.errors) + + def test_import_invalid_choice(self): + """ + Attempt to import an object with an invalid choice selection. + """ + form_data = { + 'name': 'Site 1', + 'slug': 'site-1', + 'cf_select': 'Choice X' + } + + form = SiteCSVForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('cf_select', form.errors) diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 130f94298..5ef96faa2 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -7,6 +7,7 @@ from extras.constants import GRAPH_MODELS from extras.filters import * from extras.models import ConfigContext, ExportTemplate, Graph from tenancy.models import Tenant, TenantGroup +from virtualization.models import Cluster, ClusterGroup, ClusterType class GraphTestCase(TestCase): @@ -107,6 +108,21 @@ class ConfigContextTestCase(TestCase): ) Platform.objects.bulk_create(platforms) + cluster_groups = ( + ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), + ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), + ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), + ) + ClusterGroup.objects.bulk_create(cluster_groups) + + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + clusters = ( + Cluster(name='Cluster 1', type=cluster_type), + Cluster(name='Cluster 2', type=cluster_type), + Cluster(name='Cluster 3', type=cluster_type), + ) + Cluster.objects.bulk_create(clusters) + tenant_groups = ( TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), TenantGroup(name='Tenant Group 2', slug='tenant-group-2'), @@ -132,6 +148,8 @@ class ConfigContextTestCase(TestCase): c.sites.set([sites[i]]) c.roles.set([device_roles[i]]) c.platforms.set([platforms[i]]) + c.cluster_groups.set([cluster_groups[i]]) + c.clusters.set([clusters[i]]) c.tenant_groups.set([tenant_groups[i]]) c.tenants.set([tenants[i]]) @@ -173,6 +191,18 @@ class ConfigContextTestCase(TestCase): params = {'platform': [platforms[0].slug, platforms[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cluster_group(self): + cluster_groups = ClusterGroup.objects.all()[:2] + params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cluster(self): + clusters = Cluster.objects.all()[:2] + params = {'cluster_id': [clusters[0].pk, clusters[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant_group(self): tenant_groups = TenantGroup.objects.all()[:2] params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 792390121..ecb25a78c 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -2,86 +2,102 @@ import urllib.parse import uuid from django.contrib.auth.models import User -from django.test import Client, TestCase from django.urls import reverse from dcim.models import Site from extras.choices import ObjectChangeActionChoices from extras.models import ConfigContext, ObjectChange, Tag -from utilities.testing import create_test_user +from utilities.testing import StandardTestCases, TestCase -class TagTestCase(TestCase): +class TagTestCase(StandardTestCases.Views): + model = Tag - def setUp(self): - user = create_test_user(permissions=['extras.view_tag']) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_create_object = None + test_import_objects = None - Tag.objects.bulk_create([ + @classmethod + def setUpTestData(cls): + + Tag.objects.bulk_create(( Tag(name='Tag 1', slug='tag-1'), Tag(name='Tag 2', slug='tag-2'), Tag(name='Tag 3', slug='tag-3'), - ]) + )) - def test_tag_list(self): - - url = reverse('extras:tag_list') - params = { - "q": "tag", + cls.form_data = { + 'name': 'Tag X', + 'slug': 'tag-x', + 'color': 'c0c0c0', + 'comments': 'Some comments', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'color': '00ff00', + } -class ConfigContextTestCase(TestCase): +class ConfigContextTestCase(StandardTestCases.Views): + model = ConfigContext - def setUp(self): - user = create_test_user(permissions=['extras.view_configcontext']) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_import_objects = None - site = Site(name='Site 1', slug='site-1') - site.save() + # TODO: Resolve model discrepancies when creating/editing ConfigContexts + test_create_object = None + test_edit_object = None + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') # Create three ConfigContexts for i in range(1, 4): configcontext = ConfigContext( name='Config Context {}'.format(i), - data='{{"foo": {}}}'.format(i) + data={'foo': i} ) configcontext.save() configcontext.sites.add(site) - def test_configcontext_list(self): - - url = reverse('extras:configcontext_list') - params = { - "q": "foo", + cls.form_data = { + 'name': 'Config Context X', + 'weight': 200, + 'description': 'A new config context', + 'is_active': True, + 'regions': [], + 'sites': [site.pk], + 'roles': [], + 'platforms': [], + 'tenant_groups': [], + 'tenants': [], + 'tags': [], + 'data': '{"foo": 123}', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_configcontext(self): - - configcontext = ConfigContext.objects.first() - response = self.client.get(configcontext.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'weight': 300, + 'is_active': False, + 'description': 'New description', + } +# TODO: Convert to StandardTestCases.Views class ObjectChangeTestCase(TestCase): + user_permissions = ( + 'extras.view_objectchange', + ) - def setUp(self): - user = create_test_user(permissions=['extras.view_objectchange']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() # Create three ObjectChanges + user = User.objects.create_user(username='testuser2') for i in range(1, 4): oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE) oc.user = user @@ -96,10 +112,10 @@ class ObjectChangeTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_objectchange(self): objectchange = ObjectChange.objects.first() response = self.client.get(objectchange.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index edc3ffcad..a486ce7fc 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -8,38 +8,38 @@ app_name = 'extras' urlpatterns = [ # Tags - path(r'tags/', views.TagListView.as_view(), name='tag_list'), - path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), - path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), - path(r'tags//', views.TagView.as_view(), name='tag'), - path(r'tags//edit/', views.TagEditView.as_view(), name='tag_edit'), - path(r'tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), - path(r'tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), + path('tags/', views.TagListView.as_view(), name='tag_list'), + path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), + path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), + path('tags//', views.TagView.as_view(), name='tag'), + path('tags//edit/', views.TagEditView.as_view(), name='tag_edit'), + path('tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), + path('tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), # Config contexts - path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), - path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'), - path(r'config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), - path(r'config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), - path(r'config-contexts//', views.ConfigContextView.as_view(), name='configcontext'), - path(r'config-contexts//edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), - path(r'config-contexts//delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), + path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), + path('config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'), + path('config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), + path('config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), + path('config-contexts//', views.ConfigContextView.as_view(), name='configcontext'), + path('config-contexts//edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), + path('config-contexts//delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), # Image attachments - path(r'image-attachments//edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), - path(r'image-attachments//delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), + path('image-attachments//edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), + path('image-attachments//delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), # Change logging - path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), - path(r'changelog//', views.ObjectChangeView.as_view(), name='objectchange'), + path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), + path('changelog//', views.ObjectChangeView.as_view(), name='objectchange'), # Reports - path(r'reports/', views.ReportListView.as_view(), name='report_list'), - path(r'reports//', views.ReportView.as_view(), name='report'), - path(r'reports//run/', views.ReportRunView.as_view(), name='report_run'), + path('reports/', views.ReportListView.as_view(), name='report_list'), + path('reports//', views.ReportView.as_view(), name='report'), + path('reports//run/', views.ReportRunView.as_view(), name='report_run'), # Scripts - path(r'scripts/', views.ScriptListView.as_view(), name='script_list'), - path(r'scripts///', views.ScriptView.as_view(), name='script'), + path('scripts/', views.ScriptListView.as_view(), name='script_list'), + path('scripts///', views.ScriptView.as_view(), name='script'), ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2fce98cc4..73d29393f 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -37,7 +37,8 @@ class TagListView(PermissionRequiredMixin, ObjectListView): template_name = 'extras/tag_list.html' -class TagView(View): +class TagView(PermissionRequiredMixin, View): + permission_required = 'extras.view_tag' def get(self, request, slug): @@ -84,10 +85,9 @@ class TagBulkEditView(PermissionRequiredMixin, BulkEditView): ).order_by( 'name' ) - # filter = filters.ProviderFilter table = TagTable form = forms.TagBulkEditForm - default_return_url = 'circuits:provider_list' + default_return_url = 'extras:tag_list' class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index 04eca4dfe..cfa05d0f6 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -3,6 +3,7 @@ import hashlib import hmac from django.contrib.contenttypes.models import ContentType +from django.utils import timezone from extras.models import Webhook from utilities.api import get_serializer_for_model @@ -62,7 +63,7 @@ def enqueue_webhooks(instance, user, request_id, action): serializer.data, instance._meta.model_name, action, - str(datetime.datetime.now()), + str(timezone.now()), user.username, request_id ) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 44f67d538..e52c172e5 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -237,7 +237,7 @@ class AvailableIPSerializer(serializers.Serializer): # Services # -class ServiceSerializer(CustomFieldModelSerializer): +class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer): device = NestedDeviceSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceField(choices=ServiceProtocolChoices) @@ -247,10 +247,11 @@ class ServiceSerializer(CustomFieldModelSerializer): required=False, many=True ) + tags = TagListSerializerField(required=False) class Meta: model = Service fields = [ - 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', + 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 9a2e1bc1f..c4d68f9c0 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -15,30 +15,30 @@ router = routers.DefaultRouter() router.APIRootView = IPAMRootView # Field choices -router.register(r'_choices', views.IPAMFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.IPAMFieldChoicesViewSet, basename='field-choice') # VRFs -router.register(r'vrfs', views.VRFViewSet) +router.register('vrfs', views.VRFViewSet) # RIRs -router.register(r'rirs', views.RIRViewSet) +router.register('rirs', views.RIRViewSet) # Aggregates -router.register(r'aggregates', views.AggregateViewSet) +router.register('aggregates', views.AggregateViewSet) # Prefixes -router.register(r'roles', views.RoleViewSet) -router.register(r'prefixes', views.PrefixViewSet) +router.register('roles', views.RoleViewSet) +router.register('prefixes', views.PrefixViewSet) # IP addresses -router.register(r'ip-addresses', views.IPAddressViewSet) +router.register('ip-addresses', views.IPAddressViewSet) # VLANs -router.register(r'vlan-groups', views.VLANGroupViewSet) -router.register(r'vlans', views.VLANViewSet) +router.register('vlan-groups', views.VLANGroupViewSet) +router.register('vlans', views.VLANViewSet) # Services -router.register(r'services', views.ServiceViewSet) +router.register('services', views.ServiceViewSet) app_name = 'ipam-api' urlpatterns = router.urls diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index cf6eb2a2a..41075e54a 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -4,10 +4,34 @@ from .choices import IPAddressRoleChoices BGP_ASN_MIN = 1 BGP_ASN_MAX = 2**32 - 1 + # -# IP addresses +# VRFs # +# Per RFC 4364 section 4.2, a route distinguisher may be encoded as one of the following: +# * Type 0 (16-bit AS number : 32-bit integer) +# * Type 1 (32-bit IPv4 address : 16-bit integer) +# * Type 2 (32-bit AS number : 16-bit integer) +# 21 characters are sufficient to convey the longest possible string value (255.255.255.255:65535) +VRF_RD_MAX_LENGTH = 21 + + +# +# Prefixes +# + +PREFIX_LENGTH_MIN = 1 +PREFIX_LENGTH_MAX = 127 # IPv6 + + +# +# IPAddresses +# + +IPADDRESS_MASK_LENGTH_MIN = 1 +IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6 + IPADDRESS_ROLES_NONUNIQUE = ( # IPAddress roles which are exempt from unique address enforcement IPAddressRoleChoices.ROLE_ANYCAST, @@ -17,3 +41,21 @@ IPADDRESS_ROLES_NONUNIQUE = ( IPAddressRoleChoices.ROLE_GLBP, IPAddressRoleChoices.ROLE_CARP, ) + + +# +# VLANs +# + +# 12-bit VLAN ID (values 0 and 4095 are reserved) +VLAN_VID_MIN = 1 +VLAN_VID_MAX = 4094 + + +# +# Services +# + +# 16-bit port number +SERVICE_PORT_MIN = 1 +SERVICE_PORT_MAX = 65535 diff --git a/netbox/ipam/fixtures/ipam.json b/netbox/ipam/fixtures/ipam.json deleted file mode 100644 index e722b3629..000000000 --- a/netbox/ipam/fixtures/ipam.json +++ /dev/null @@ -1,329 +0,0 @@ -[ -{ - "model": "ipam.rir", - "pk": 1, - "fields": { - "name": "RFC1918", - "slug": "rfc1918" - } -}, -{ - "model": "ipam.aggregate", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.0.0.0/8", - "rir": 1, - "date_added": null, - "description": "" - } -}, -{ - "model": "ipam.role", - "pk": 1, - "fields": { - "name": "Lab Network", - "slug": "lab-network", - "weight": 1000 - } -}, -{ - "model": "ipam.prefix", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.1.1.0/24", - "site": 1, - "vrf": null, - "vlan": null, - "status": "active", - "role": 1, - "description": "" - } -}, -{ - "model": "ipam.prefix", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.0.255.0/24", - "site": 1, - "vrf": null, - "vlan": null, - "status": "active", - "role": 1, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.255.1/32", - "vrf": null, - "interface_id": 3, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.254.1/31", - "vrf": null, - "interface_id": 4, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.255.2/32", - "vrf": null, - "interface_id": 185, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.1/31", - "vrf": null, - "interface_id": 213, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.254.1/24", - "vrf": null, - "interface_id": 12, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 8, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.21.1/31", - "vrf": null, - "interface_id": 218, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 9, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.21.2/31", - "vrf": null, - "interface_id": 9, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 10, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.1/31", - "vrf": null, - "interface_id": 8, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 11, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.20.1/31", - "vrf": null, - "interface_id": 7, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 12, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.20.1/31", - "vrf": null, - "interface_id": 216, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 13, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.2/31", - "vrf": null, - "interface_id": 206, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 14, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.22.1/31", - "vrf": null, - "interface_id": 217, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 15, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.22.2/31", - "vrf": null, - "interface_id": 205, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 16, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.20.2/31", - "vrf": null, - "interface_id": 211, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 17, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.2/31", - "vrf": null, - "interface_id": 212, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 19, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.254.2/32", - "vrf": null, - "interface_id": 188, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 20, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.1/31", - "vrf": null, - "interface_id": 200, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 21, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.2/31", - "vrf": null, - "interface_id": 194, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.vlan", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "site": 1, - "vid": 999, - "name": "TEST", - "status": "active", - "role": 1 - } -} -] \ No newline at end of file diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 265ddcb7b..24f044f79 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -4,33 +4,36 @@ from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField from dcim.models import Device, Interface, Rack, Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, +) from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, - SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES ) from virtualization.models import VirtualMachine +from .constants import * from .choices import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -IP_FAMILY_CHOICES = [ - ('', 'All'), - (4, 'IPv4'), - (6, 'IPv6'), -] -PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)]) -IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)]) +PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([ + (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1) +]) + +IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ + (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1) +]) # # VRFs # -class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): tags = TagField( required=False ) @@ -48,7 +51,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VRFCSVForm(forms.ModelForm): +class VRFCSVForm(CustomFieldModelCSVForm): tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -102,6 +105,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): required=False, label='Search' ) + tag = TagFilterField(model) # @@ -143,7 +147,7 @@ class RIRFilterForm(BootstrapMixin, forms.Form): # Aggregates # -class AggregateForm(BootstrapMixin, CustomFieldForm): +class AggregateForm(BootstrapMixin, CustomFieldModelForm): tags = TagField( required=False ) @@ -165,7 +169,7 @@ class AggregateForm(BootstrapMixin, CustomFieldForm): } -class AggregateCSVForm(forms.ModelForm): +class AggregateCSVForm(CustomFieldModelCSVForm): rir = forms.ModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -218,7 +222,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): ) family = forms.ChoiceField( required=False, - choices=IP_FAMILY_CHOICES, + choices=add_blank_choice(IPAddressFamilyChoices), label='Address family', widget=StaticSelect2() ) @@ -231,6 +235,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) + tag = TagFilterField(model) # @@ -262,7 +267,7 @@ class RoleCSVForm(forms.ModelForm): # Prefixes # -class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -340,7 +345,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class PrefixCSVForm(forms.ModelForm): +class PrefixCSVForm(CustomFieldModelCSVForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -450,8 +455,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ) ) prefix_length = forms.IntegerField( - min_value=1, - max_value=127, + min_value=PREFIX_LENGTH_MIN, + max_value=PREFIX_LENGTH_MAX, required=False ) tenant = forms.ModelChoiceField( @@ -510,7 +515,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) ) family = forms.ChoiceField( required=False, - choices=IP_FAMILY_CHOICES, + choices=add_blank_choice(IPAddressFamilyChoices), label='Address family', widget=StaticSelect2() ) @@ -577,13 +582,14 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) required=False, label='Expand prefix hierarchy' ) + tag = TagFilterField(model) # # IP addresses # -class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm): +class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm): interface = forms.ModelChoiceField( queryset=Interface.objects.all(), required=False @@ -634,6 +640,17 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) } ) ) + nat_vrf = forms.ModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF', + widget=APISelect( + api_url="/api/ipam/vrfs/", + filter_for={ + 'nat_inside': 'vrf_id' + } + ) + ) nat_inside = ChainedModelChoiceField( queryset=IPAddress.objects.all(), chains=( @@ -739,7 +756,7 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): ) -class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class Meta: model = IPAddress @@ -759,7 +776,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class IPAddressCSVForm(forms.ModelForm): +class IPAddressCSVForm(CustomFieldModelCSVForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -896,8 +913,8 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd ) ) mask_length = forms.IntegerField( - min_value=1, - max_value=128, + min_value=IPADDRESS_MASK_LENGTH_MIN, + max_value=IPADDRESS_MASK_LENGTH_MAX, required=False ) tenant = forms.ModelChoiceField( @@ -969,7 +986,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo ) family = forms.ChoiceField( required=False, - choices=IP_FAMILY_CHOICES, + choices=add_blank_choice(IPAddressFamilyChoices), label='Address family', widget=StaticSelect2() ) @@ -1005,6 +1022,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -1075,7 +1093,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): # VLANs # -class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1123,7 +1141,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VLANCSVForm(forms.ModelForm): +class VLANCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1292,16 +1310,17 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # # Services # -class ServiceForm(BootstrapMixin, CustomFieldForm): +class ServiceForm(BootstrapMixin, CustomFieldModelForm): port = forms.IntegerField( - min_value=1, - max_value=65535 + min_value=SERVICE_PORT_MIN, + max_value=SERVICE_PORT_MAX ) tags = TagField( required=False @@ -1352,6 +1371,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): port = forms.IntegerField( required=False, ) + tag = TagFilterField(model) class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): @@ -1378,5 +1398,5 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class Meta: nullable_fields = [ - 'site', 'tenant', 'role', 'description', + 'description', ] diff --git a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py index 528efb4fb..195b630db 100644 --- a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py +++ b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py @@ -2,10 +2,10 @@ from django.db import migrations, models IPADDRESS_STATUS_CHOICES = ( - (0, 'container'), (1, 'active'), (2, 'reserved'), (3, 'deprecated'), + (5, 'dhcp'), ) IPADDRESS_ROLE_CHOICES = ( diff --git a/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py new file mode 100644 index 000000000..9e496153e --- /dev/null +++ b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +def ipaddress_status_dhcp_to_slug(apps, schema_editor): + IPAddress = apps.get_model('ipam', 'IPAddress') + IPAddress.objects.filter(status='5').update(status='dhcp') + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0033_deterministic_ordering'), + ] + + operations = [ + # Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed, + # so this can be omitted when squashing in the future. + migrations.RunPython( + code=ipaddress_status_dhcp_to_slug + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 885d5d617..b4ba92fb5 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -14,7 +14,7 @@ from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from virtualization.models import VirtualMachine from .choices import * -from .constants import IPADDRESS_ROLES_NONUNIQUE +from .constants import * from .fields import IPNetworkField, IPAddressField from .managers import IPAddressManager from .querysets import PrefixQuerySet @@ -44,7 +44,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel): max_length=50 ) rd = models.CharField( - max_length=21, + max_length=VRF_RD_MAX_LENGTH, unique=True, blank=True, null=True, @@ -1006,7 +1006,7 @@ class Service(ChangeLoggedModel, CustomFieldModel): choices=ServiceProtocolChoices ) port = models.PositiveIntegerField( - validators=[MinValueValidator(1), MaxValueValidator(65535)], + validators=[MinValueValidator(SERVICE_PORT_MIN), MaxValueValidator(SERVICE_PORT_MAX)], verbose_name='Port number' ) ipaddresses = models.ManyToManyField( diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 983787b0c..99a7eaca4 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1064,6 +1064,7 @@ class ServiceTest(APITestCase): 'name': 'Test Service 4', 'protocol': ServiceProtocolChoices.PROTOCOL_TCP, 'port': 4, + 'tags': ['Foo', 'Bar'], } url = reverse('ipam-api:service-list') @@ -1076,6 +1077,8 @@ class ServiceTest(APITestCase): self.assertEqual(service4.name, data['name']) self.assertEqual(service4.protocol, data['protocol']) self.assertEqual(service4.port, data['port']) + tags = [tag.name for tag in service4.tags.all()] + self.assertEqual(sorted(tags), sorted(data['tags'])) def test_create_service_bulk(self): diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 6f08f2d47..cfa06788c 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -1,26 +1,18 @@ -from netaddr import IPNetwork -import urllib.parse +import datetime -from django.test import Client, TestCase -from django.urls import reverse +from netaddr import IPNetwork from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site -from ipam.choices import ServiceProtocolChoices +from ipam.choices import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -from utilities.testing import create_test_user +from utilities.testing import StandardTestCases -class VRFTestCase(TestCase): +class VRFTestCase(StandardTestCases.Views): + model = VRF - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vrf', - 'ipam.add_vrf', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): VRF.objects.bulk_create([ VRF(name='VRF 1', rd='65000:1'), @@ -28,48 +20,39 @@ class VRFTestCase(TestCase): VRF(name='VRF 3', rd='65000:3'), ]) - def test_vrf_list(self): - - url = reverse('ipam:vrf_list') - params = { - "q": "65000", + cls.form_data = { + 'name': 'VRF X', + 'rd': '65000:999', + 'tenant': None, + 'enforce_unique': True, + 'description': 'A new VRF', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_vrf(self): - - vrf = VRF.objects.first() - response = self.client.get(vrf.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_vrf_import(self): - - csv_data = ( + cls.csv_data = ( "name", "VRF 4", "VRF 5", "VRF 6", ) - response = self.client.post(reverse('ipam:vrf_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(VRF.objects.count(), 6) + cls.bulk_edit_data = { + 'tenant': None, + 'enforce_unique': False, + 'description': 'New description', + } -class RIRTestCase(TestCase): +class RIRTestCase(StandardTestCases.Views): + model = RIR - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_rir', - 'ipam.add_rir', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): RIR.objects.bulk_create([ RIR(name='RIR 1', slug='rir-1'), @@ -77,91 +60,71 @@ class RIRTestCase(TestCase): RIR(name='RIR 3', slug='rir-3'), ]) - def test_rir_list(self): + cls.form_data = { + 'name': 'RIR X', + 'slug': 'rir-x', + 'is_private': True, + } - url = reverse('ipam:rir_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_rir_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "RIR 4,rir-4", "RIR 5,rir-5", "RIR 6,rir-6", ) - response = self.client.post(reverse('ipam:rir_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(RIR.objects.count(), 6) +class AggregateTestCase(StandardTestCases.Views): + model = Aggregate + @classmethod + def setUpTestData(cls): -class AggregateTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_aggregate', - 'ipam.add_aggregate', - ] + rirs = ( + RIR(name='RIR 1', slug='rir-1'), + RIR(name='RIR 2', slug='rir-2'), ) - self.client = Client() - self.client.force_login(user) - - rir = RIR(name='RIR 1', slug='rir-1') - rir.save() + RIR.objects.bulk_create(rirs) Aggregate.objects.bulk_create([ - Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir), - Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir), - Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir), + Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rirs[0]), + Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rirs[0]), + Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]), ]) - def test_aggregate_list(self): - - url = reverse('ipam:aggregate_list') - params = { - "rir": RIR.objects.first().slug, + cls.form_data = { + 'family': 4, + 'prefix': IPNetwork('10.99.0.0/16'), + 'rir': rirs[1].pk, + 'date_added': datetime.date(2020, 1, 1), + 'description': 'A new aggregate', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_aggregate(self): - - aggregate = Aggregate.objects.first() - response = self.client.get(aggregate.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_aggregate_import(self): - - csv_data = ( + cls.csv_data = ( "prefix,rir", "10.4.0.0/16,RIR 1", "10.5.0.0/16,RIR 1", "10.6.0.0/16,RIR 1", ) - response = self.client.post(reverse('ipam:aggregate_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Aggregate.objects.count(), 6) + cls.bulk_edit_data = { + 'rir': rirs[1].pk, + 'date_added': datetime.date(2020, 1, 1), + 'description': 'New description', + } -class RoleTestCase(TestCase): +class RoleTestCase(StandardTestCases.Views): + model = Role - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_role', - 'ipam.add_role', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): Role.objects.bulk_create([ Role(name='Role 1', slug='role-1'), @@ -169,146 +132,140 @@ class RoleTestCase(TestCase): Role(name='Role 3', slug='role-3'), ]) - def test_role_list(self): + cls.form_data = { + 'name': 'Role X', + 'slug': 'role-x', + 'weight': 200, + 'description': 'A new role', + } - url = reverse('ipam:role_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_role_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug,weight", "Role 4,role-4,1000", "Role 5,role-5,1000", "Role 6,role-6,1000", ) - response = self.client.post(reverse('ipam:role_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(Role.objects.count(), 6) +class PrefixTestCase(StandardTestCases.Views): + model = Prefix + @classmethod + def setUpTestData(cls): -class PrefixTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_prefix', - 'ipam.add_prefix', - ] + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), ) - self.client = Client() - self.client.force_login(user) + Site.objects.bulk_create(sites) - site = Site(name='Site 1', slug='site-1') - site.save() + vrfs = ( + VRF(name='VRF 1', rd='65000:1'), + VRF(name='VRF 2', rd='65000:2'), + ) + VRF.objects.bulk_create(vrfs) + + roles = ( + Role(name='Role 1', slug='role-1'), + Role(name='Role 2', slug='role-2'), + ) Prefix.objects.bulk_create([ - Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site), - Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site), - Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site), + Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), ]) - def test_prefix_list(self): - - url = reverse('ipam:prefix_list') - params = { - "site": Site.objects.first().slug, + cls.form_data = { + 'prefix': IPNetwork('192.0.2.0/24'), + 'site': sites[1].pk, + 'vrf': vrfs[1].pk, + 'tenant': None, + 'vlan': None, + 'status': PrefixStatusChoices.STATUS_RESERVED, + 'role': roles[1].pk, + 'is_pool': True, + 'description': 'A new prefix', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_prefix(self): - - prefix = Prefix.objects.first() - response = self.client.get(prefix.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_prefix_import(self): - - csv_data = ( + cls.csv_data = ( "prefix,status", "10.4.0.0/16,Active", "10.5.0.0/16,Active", "10.6.0.0/16,Active", ) - response = self.client.post(reverse('ipam:prefix_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Prefix.objects.count(), 6) - - -class IPAddressTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_ipaddress', - 'ipam.add_ipaddress', - ] - ) - self.client = Client() - self.client.force_login(user) - - vrf = VRF(name='VRF 1', rd='65000:1') - vrf.save() - - IPAddress.objects.bulk_create([ - IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf), - IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrf), - IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf), - ]) - - def test_ipaddress_list(self): - - url = reverse('ipam:ipaddress_list') - params = { - "vrf": VRF.objects.first().rd, + cls.bulk_edit_data = { + 'site': sites[1].pk, + 'vrf': vrfs[1].pk, + 'tenant': None, + 'status': PrefixStatusChoices.STATUS_RESERVED, + 'role': roles[1].pk, + 'is_pool': False, + 'description': 'New description', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - def test_ipaddress(self): +class IPAddressTestCase(StandardTestCases.Views): + model = IPAddress - ipaddress = IPAddress.objects.first() - response = self.client.get(ipaddress.get_absolute_url()) - self.assertEqual(response.status_code, 200) + @classmethod + def setUpTestData(cls): - def test_ipaddress_import(self): + vrfs = ( + VRF(name='VRF 1', rd='65000:1'), + VRF(name='VRF 2', rd='65000:2'), + ) - csv_data = ( + IPAddress.objects.bulk_create([ + IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]), + IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrfs[0]), + IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]), + ]) + + cls.form_data = { + 'vrf': vrfs[1].pk, + 'address': IPNetwork('192.0.2.99/24'), + 'tenant': None, + 'status': IPAddressStatusChoices.STATUS_RESERVED, + 'role': IPAddressRoleChoices.ROLE_ANYCAST, + 'interface': None, + 'nat_inside': None, + 'dns_name': 'example', + 'description': 'A new IP address', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.csv_data = ( "address,status", "192.0.2.4/24,Active", "192.0.2.5/24,Active", "192.0.2.6/24,Active", ) - response = self.client.post(reverse('ipam:ipaddress_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(IPAddress.objects.count(), 6) + cls.bulk_edit_data = { + 'vrf': vrfs[1].pk, + 'tenant': None, + 'status': IPAddressStatusChoices.STATUS_RESERVED, + 'role': IPAddressRoleChoices.ROLE_ANYCAST, + 'dns_name': 'example', + 'description': 'New description', + } -class VLANGroupTestCase(TestCase): +class VLANGroupTestCase(StandardTestCases.Views): + model = VLANGroup - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vlangroup', - 'ipam.add_vlangroup', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None - site = Site(name='Site 1', slug='site-1') - site.save() + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') VLANGroup.objects.bulk_create([ VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site), @@ -316,104 +273,96 @@ class VLANGroupTestCase(TestCase): VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site), ]) - def test_vlangroup_list(self): - - url = reverse('ipam:vlangroup_list') - params = { - "site": Site.objects.first().slug, + cls.form_data = { + 'name': 'VLAN Group X', + 'slug': 'vlan-group-x', + 'site': site.pk, } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_vlangroup_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "VLAN Group 4,vlan-group-4", "VLAN Group 5,vlan-group-5", "VLAN Group 6,vlan-group-6", ) - response = self.client.post(reverse('ipam:vlangroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(VLANGroup.objects.count(), 6) +class VLANTestCase(StandardTestCases.Views): + model = VLAN + @classmethod + def setUpTestData(cls): -class VLANTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vlan', - 'ipam.add_vlan', - ] + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), ) - self.client = Client() - self.client.force_login(user) + Site.objects.bulk_create(sites) - vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1') - vlangroup.save() + vlangroups = ( + VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]), + VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]), + ) + VLANGroup.objects.bulk_create(vlangroups) + + roles = ( + Role(name='Role 1', slug='role-1'), + Role(name='Role 2', slug='role-2'), + ) + Role.objects.bulk_create(roles) VLAN.objects.bulk_create([ - VLAN(group=vlangroup, vid=101, name='VLAN101'), - VLAN(group=vlangroup, vid=102, name='VLAN102'), - VLAN(group=vlangroup, vid=103, name='VLAN103'), + VLAN(group=vlangroups[0], vid=101, name='VLAN101', site=sites[0], role=roles[0]), + VLAN(group=vlangroups[0], vid=102, name='VLAN102', site=sites[0], role=roles[0]), + VLAN(group=vlangroups[0], vid=103, name='VLAN103', site=sites[0], role=roles[0]), ]) - def test_vlan_list(self): - - url = reverse('ipam:vlan_list') - params = { - "group": VLANGroup.objects.first().slug, + cls.form_data = { + 'site': sites[1].pk, + 'group': vlangroups[1].pk, + 'vid': 999, + 'name': 'VLAN999', + 'tenant': None, + 'status': VLANStatusChoices.STATUS_RESERVED, + 'role': roles[1].pk, + 'description': 'A new VLAN', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_vlan(self): - - vlan = VLAN.objects.first() - response = self.client.get(vlan.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_vlan_import(self): - - csv_data = ( + cls.csv_data = ( "vid,name,status", "104,VLAN104,Active", "105,VLAN105,Active", "106,VLAN106,Active", ) - response = self.client.post(reverse('ipam:vlan_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(VLAN.objects.count(), 6) + cls.bulk_edit_data = { + 'site': sites[1].pk, + 'group': vlangroups[1].pk, + 'tenant': None, + 'status': VLANStatusChoices.STATUS_RESERVED, + 'role': roles[1].pk, + 'description': 'New description', + } -class ServiceTestCase(TestCase): +class ServiceTestCase(StandardTestCases.Views): + model = Service - def setUp(self): - user = create_test_user(permissions=['ipam.view_service']) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_import_objects = None - site = Site(name='Site 1', slug='site-1') - site.save() + # TODO: Resolve URL for Service creation + test_create_object = None - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() + @classmethod + def setUpTestData(cls): - devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1') - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + site = Site.objects.create(name='Site 1', slug='site-1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1') + devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) Service.objects.bulk_create([ Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101), @@ -421,18 +370,19 @@ class ServiceTestCase(TestCase): Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103), ]) - def test_service_list(self): - - url = reverse('ipam:service_list') - params = { - "device_id": Device.objects.first(), + cls.form_data = { + 'device': device.pk, + 'virtual_machine': None, + 'name': 'Service X', + 'protocol': ServiceProtocolChoices.PROTOCOL_TCP, + 'port': 999, + 'ipaddresses': [], + 'description': 'A new service', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_service(self): - - service = Service.objects.first() - response = self.client.get(service.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'protocol': ServiceProtocolChoices.PROTOCOL_UDP, + 'port': 888, + 'description': 'New description', + } diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 2a1dcdf05..604287f24 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -8,97 +8,97 @@ app_name = 'ipam' urlpatterns = [ # VRFs - path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'), - path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'), - path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'), - path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), - path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), - path(r'vrfs//', views.VRFView.as_view(), name='vrf'), - path(r'vrfs//edit/', views.VRFEditView.as_view(), name='vrf_edit'), - path(r'vrfs//delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), - path(r'vrfs//changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), + path('vrfs/', views.VRFListView.as_view(), name='vrf_list'), + path('vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'), + path('vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'), + path('vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), + path('vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), + path('vrfs//', views.VRFView.as_view(), name='vrf'), + path('vrfs//edit/', views.VRFEditView.as_view(), name='vrf_edit'), + path('vrfs//delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), + path('vrfs//changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), # RIRs - path(r'rirs/', views.RIRListView.as_view(), name='rir_list'), - path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'), - path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), - path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), - path(r'rirs//edit/', views.RIREditView.as_view(), name='rir_edit'), - path(r'vrfs//changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), + path('rirs/', views.RIRListView.as_view(), name='rir_list'), + path('rirs/add/', views.RIRCreateView.as_view(), name='rir_add'), + path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), + path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), + path('rirs//edit/', views.RIREditView.as_view(), name='rir_edit'), + path('vrfs//changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), # Aggregates - path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), - path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'), - path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'), - path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), - path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), - path(r'aggregates//', views.AggregateView.as_view(), name='aggregate'), - path(r'aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), - path(r'aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), - path(r'aggregates//changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), + path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), + path('aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'), + path('aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'), + path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), + path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), + path('aggregates//', views.AggregateView.as_view(), name='aggregate'), + path('aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), + path('aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), + path('aggregates//changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), # Roles - path(r'roles/', views.RoleListView.as_view(), name='role_list'), - path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'), - path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), - path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), - path(r'roles//edit/', views.RoleEditView.as_view(), name='role_edit'), - path(r'roles//changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), + path('roles/', views.RoleListView.as_view(), name='role_list'), + path('roles/add/', views.RoleCreateView.as_view(), name='role_add'), + path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), + path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), + path('roles//edit/', views.RoleEditView.as_view(), name='role_edit'), + path('roles//changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), # Prefixes - path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'), - path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'), - path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'), - path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), - path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), - path(r'prefixes//', views.PrefixView.as_view(), name='prefix'), - path(r'prefixes//edit/', views.PrefixEditView.as_view(), name='prefix_edit'), - path(r'prefixes//delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), - path(r'prefixes//changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), - path(r'prefixes//prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), - path(r'prefixes//ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), + path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'), + path('prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'), + path('prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'), + path('prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), + path('prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), + path('prefixes//', views.PrefixView.as_view(), name='prefix'), + path('prefixes//edit/', views.PrefixEditView.as_view(), name='prefix_edit'), + path('prefixes//delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), + path('prefixes//changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), + path('prefixes//prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), + path('prefixes//ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), # IP addresses - path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), - path(r'ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'), - path(r'ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), - path(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), - path(r'ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), - path(r'ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), - path(r'ip-addresses//changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), - path(r'ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), - path(r'ip-addresses//', views.IPAddressView.as_view(), name='ipaddress'), - path(r'ip-addresses//edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), - path(r'ip-addresses//delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), + path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), + path('ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'), + path('ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), + path('ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), + path('ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), + path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), + path('ip-addresses//changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), + path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), + path('ip-addresses//', views.IPAddressView.as_view(), name='ipaddress'), + path('ip-addresses//edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), + path('ip-addresses//delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), # VLAN groups - path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'), - path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), - path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), - path(r'vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), - path(r'vlan-groups//edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), - path(r'vlan-groups//vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), - path(r'vlan-groups//changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), + path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'), + path('vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), + path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), + path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), + path('vlan-groups//edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), + path('vlan-groups//vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), + path('vlan-groups//changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), # VLANs - path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'), - path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'), - path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'), - path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), - path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), - path(r'vlans//', views.VLANView.as_view(), name='vlan'), - path(r'vlans//members/', views.VLANMembersView.as_view(), name='vlan_members'), - path(r'vlans//edit/', views.VLANEditView.as_view(), name='vlan_edit'), - path(r'vlans//delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), - path(r'vlans//changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), + path('vlans/', views.VLANListView.as_view(), name='vlan_list'), + path('vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'), + path('vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'), + path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), + path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), + path('vlans//', views.VLANView.as_view(), name='vlan'), + path('vlans//members/', views.VLANMembersView.as_view(), name='vlan_members'), + path('vlans//edit/', views.VLANEditView.as_view(), name='vlan_edit'), + path('vlans//delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), + path('vlans//changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), # Services - path(r'services/', views.ServiceListView.as_view(), name='service_list'), - path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), - path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), - path(r'services//', views.ServiceView.as_view(), name='service'), - path(r'services//edit/', views.ServiceEditView.as_view(), name='service_edit'), - path(r'services//delete/', views.ServiceDeleteView.as_view(), name='service_delete'), - path(r'services//changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), + path('services/', views.ServiceListView.as_view(), name='service_list'), + path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), + path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), + path('services//', views.ServiceView.as_view(), name='service'), + path('services//edit/', views.ServiceEditView.as_view(), name='service_edit'), + path('services//delete/', views.ServiceDeleteView.as_view(), name='service_delete'), + path('services//changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), ] diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 5cbd55bf4..c8c7d40ca 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -15,6 +15,7 @@ from utilities.views import ( from virtualization.models import VirtualMachine from . import filters, forms, tables from .choices import * +from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -86,23 +87,20 @@ def add_available_vlans(vlan_group, vlans): """ Create fake records for all gaps between used VLANs """ - MIN_VLAN = 1 - MAX_VLAN = 4094 - if not vlans: - return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}] + return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}] - prev_vid = MAX_VLAN + prev_vid = VLAN_VID_MAX new_vlans = [] for vlan in vlans: if vlan.vid - prev_vid > 1: new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1}) prev_vid = vlan.vid - if vlans[0].vid > MIN_VLAN: - new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN}) - if prev_vid < MAX_VLAN: - new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid}) + if vlans[0].vid > VLAN_VID_MIN: + new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN}) + if prev_vid < VLAN_VID_MAX: + new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid}) vlans = list(vlans) + new_vlans vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e5925184d..aa90bdcbb 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.7.3-dev' +VERSION = '2.7.5-dev' # Hostname HOSTNAME = platform.node() @@ -74,6 +74,7 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DEBUG = getattr(configuration, 'DEBUG', False) +DEVELOPER = getattr(configuration, 'DEVELOPER', False) EMAIL = getattr(configuration, 'EMAIL', {}) ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) @@ -503,6 +504,7 @@ SWAGGER_SETTINGS = { 'utilities.custom_inspectors.IdInFilterInspector', 'drf_yasg.inspectors.CoreAPICompatInspector', ], + 'DEFAULT_INFO': 'netbox.urls.openapi_info', 'DEFAULT_MODEL_DEPTH': 1, 'DEFAULT_PAGINATOR_INSPECTORS': [ 'utilities.custom_inspectors.NullablePaginatorInspector', diff --git a/netbox/netbox/tests/__init__.py b/netbox/netbox/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/netbox/tests/test_api.py b/netbox/netbox/tests/test_api.py new file mode 100644 index 000000000..0ee2d78dc --- /dev/null +++ b/netbox/netbox/tests/test_api.py @@ -0,0 +1,13 @@ +from django.urls import reverse + +from utilities.testing import APITestCase + + +class AppTest(APITestCase): + + def test_root(self): + + url = reverse('api-root') + response = self.client.get('{}?format=api'.format(url), **self.header) + + self.assertEqual(response.status_code, 200) diff --git a/netbox/netbox/tests/test_views.py b/netbox/netbox/tests/test_views.py new file mode 100644 index 000000000..1942471b0 --- /dev/null +++ b/netbox/netbox/tests/test_views.py @@ -0,0 +1,24 @@ +import urllib.parse + +from utilities.testing import TestCase +from django.urls import reverse + + +class HomeViewTestCase(TestCase): + + def test_home(self): + + url = reverse('home') + + response = self.client.get(url) + self.assertHttpStatus(response, 200) + + def test_search(self): + + url = reverse('search') + params = { + 'q': 'foo', + } + + response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) + self.assertHttpStatus(response, 200) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6b6dfe22d..2c4d504b2 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -9,14 +9,16 @@ from netbox.views import APIRootView, HomeView, SearchView from users.views import LoginView, LogoutView from .admin import admin_site +openapi_info = openapi.Info( + title="NetBox API", + default_version='v2', + description="API to access NetBox", + terms_of_service="https://github.com/netbox-community/netbox", + license=openapi.License(name="Apache v2 License"), +) + schema_view = get_schema_view( - openapi.Info( - title="NetBox API", - default_version='v2', - description="API to access NetBox", - terms_of_service="https://github.com/netbox-community/netbox", - license=openapi.License(name="Apache v2 License"), - ), + openapi_info, validators=['flex', 'ssv'], public=True, ) @@ -24,49 +26,49 @@ schema_view = get_schema_view( _patterns = [ # Base views - path(r'', HomeView.as_view(), name='home'), - path(r'search/', SearchView.as_view(), name='search'), + path('', HomeView.as_view(), name='home'), + path('search/', SearchView.as_view(), name='search'), # Login/logout - path(r'login/', LoginView.as_view(), name='login'), - path(r'logout/', LogoutView.as_view(), name='logout'), + path('login/', LoginView.as_view(), name='login'), + path('logout/', LogoutView.as_view(), name='logout'), # Apps - path(r'circuits/', include('circuits.urls')), - path(r'dcim/', include('dcim.urls')), - path(r'extras/', include('extras.urls')), - path(r'ipam/', include('ipam.urls')), - path(r'secrets/', include('secrets.urls')), - path(r'tenancy/', include('tenancy.urls')), - path(r'user/', include('users.urls')), - path(r'virtualization/', include('virtualization.urls')), + path('circuits/', include('circuits.urls')), + path('dcim/', include('dcim.urls')), + path('extras/', include('extras.urls')), + path('ipam/', include('ipam.urls')), + path('secrets/', include('secrets.urls')), + path('tenancy/', include('tenancy.urls')), + path('user/', include('users.urls')), + path('virtualization/', include('virtualization.urls')), # API - path(r'api/', APIRootView.as_view(), name='api-root'), - path(r'api/circuits/', include('circuits.api.urls')), - path(r'api/dcim/', include('dcim.api.urls')), - path(r'api/extras/', include('extras.api.urls')), - path(r'api/ipam/', include('ipam.api.urls')), - path(r'api/secrets/', include('secrets.api.urls')), - path(r'api/tenancy/', include('tenancy.api.urls')), - path(r'api/virtualization/', include('virtualization.api.urls')), - path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'), - path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'), + path('api/', APIRootView.as_view(), name='api-root'), + path('api/circuits/', include('circuits.api.urls')), + path('api/dcim/', include('dcim.api.urls')), + path('api/extras/', include('extras.api.urls')), + path('api/ipam/', include('ipam.api.urls')), + path('api/secrets/', include('secrets.api.urls')), + path('api/tenancy/', include('tenancy.api.urls')), + path('api/virtualization/', include('virtualization.api.urls')), + path('api/docs/', schema_view.with_ui('swagger'), name='api_docs'), + path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'), re_path(r'^api/swagger(?P.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'), # Serving static media in Django to pipe it through LoginRequiredMiddleware - path(r'media/', serve, {'document_root': settings.MEDIA_ROOT}), + path('media/', serve, {'document_root': settings.MEDIA_ROOT}), # Admin - path(r'admin/', admin_site.urls), - path(r'admin/webhook-backend-status/', include('django_rq.urls')), + path('admin/', admin_site.urls), + path('admin/webhook-backend-status/', include('django_rq.urls')), ] if settings.DEBUG: import debug_toolbar _patterns += [ - path(r'__debug__/', include(debug_toolbar.urls)), + path('__debug__/', include(debug_toolbar.urls)), ] if settings.METRICS_ENABLED: @@ -76,7 +78,7 @@ if settings.METRICS_ENABLED: # Prepend BASE_PATH urlpatterns = [ - path(r'{}'.format(settings.BASE_PATH), include(_patterns)) + path('{}'.format(settings.BASE_PATH), include(_patterns)) ] handler500 = 'utilities.views.server_error' diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index fbe70300b..904dc7375 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -252,7 +252,7 @@ class HomeView(View): 'search_form': SearchForm(), 'stats': stats, 'report_results': ReportResult.objects.order_by('-created')[:10], - 'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:50] + 'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:15] }) diff --git a/netbox/project-static/js/configcontext.js b/netbox/project-static/js/configcontext.js new file mode 100644 index 000000000..1d731e696 --- /dev/null +++ b/netbox/project-static/js/configcontext.js @@ -0,0 +1,11 @@ +$('.rendered-context-format').on('click', function() { + if (!$(this).hasClass('active')) { + // Update selection in the button group + $('span.rendered-context-format').removeClass('active'); + $('span.rendered-context-format[data-format=' + $(this).data('format') + ']').addClass('active'); + + // Hide all rendered contexts and only show the selected one + $('div.rendered-context-data').hide(); + $('div.rendered-context-data[data-format=' + $(this).data('format') + ']').show(); + } +}); diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 58783b5d0..b1ba8a37c 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -158,14 +158,17 @@ $(document).ready(function() { filter_for_elements.each(function(index, filter_for_element) { var param_name = $(filter_for_element).attr(attr_name); + var is_required = $(filter_for_element).attr("required"); var is_nullable = $(filter_for_element).attr("nullable"); var is_visible = $(filter_for_element).is(":visible"); var value = $(filter_for_element).val(); - if (param_name && is_visible && value) { - parameters[param_name] = value; - } else if (param_name && is_visible && is_nullable) { - parameters[param_name] = "null"; + if (param_name && is_visible) { + if (value) { + parameters[param_name] = value; + } else if (is_required && is_nullable) { + parameters[param_name] = "null"; + } } }); diff --git a/netbox/project-static/js/interface_toggles.js b/netbox/project-static/js/interface_toggles.js index a46d3185c..df8ac064b 100644 --- a/netbox/project-static/js/interface_toggles.js +++ b/netbox/project-static/js/interface_toggles.js @@ -2,9 +2,9 @@ $('button.toggle-ips').click(function() { var selected = $(this).attr('selected'); if (selected) { - $('#interfaces_table tr.ipaddresses').hide(); + $('#interfaces_table tr.interface:visible + tr.ipaddresses').hide(); } else { - $('#interfaces_table tr.ipaddresses').show(); + $('#interfaces_table tr.interface:visible + tr.ipaddresses').show(); } $(this).attr('selected', !selected); $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); @@ -14,17 +14,22 @@ $('button.toggle-ips').click(function() { // Inteface filtering $('input.interface-filter').on('input', function() { var filter = new RegExp(this.value); + var interface; - for (interface of $(this).closest('div.panel').find('tbody > tr')) { + for (interface of $('#interfaces_table > tbody > tr.interface')) { // Slice off 'interface_' at the start of the ID - if (filter && filter.test(interface.id.slice(10))) { + if (filter.test(interface.id.slice(10))) { // Match the toggle in case the filter now matches the interface $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked')); $(interface).show(); + if ($('button.toggle-ips').attr('selected')) { + $(interface).next('tr.ipaddresses').show(); + } } else { // Uncheck to prevent actions from including it when it doesn't match $(interface).find('input:checkbox[name=pk]').prop('checked', false); $(interface).hide(); + $(interface).next('tr.ipaddresses').hide(); } } }); diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index def87b3a1..70abcfe29 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -15,15 +15,15 @@ router = routers.DefaultRouter() router.APIRootView = SecretsRootView # Field choices -router.register(r'_choices', views.SecretsFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.SecretsFieldChoicesViewSet, basename='field-choice') # Secrets -router.register(r'secret-roles', views.SecretRoleViewSet) -router.register(r'secrets', views.SecretViewSet) +router.register('secret-roles', views.SecretRoleViewSet) +router.register('secrets', views.SecretViewSet) # Miscellaneous -router.register(r'get-session-key', views.GetSessionKeyViewSet, basename='get-session-key') -router.register(r'generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, basename='generate-rsa-key-pair') +router.register('get-session-key', views.GetSessionKeyViewSet, basename='get-session-key') +router.register('generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, basename='generate-rsa-key-pair') app_name = 'secrets-api' urlpatterns = router.urls diff --git a/netbox/secrets/constants.py b/netbox/secrets/constants.py new file mode 100644 index 000000000..a1c3cb3da --- /dev/null +++ b/netbox/secrets/constants.py @@ -0,0 +1,5 @@ +# +# Secrets +# + +SECRET_PLAINTEXT_MAX_LENGTH = 65535 diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index c937e6c92..2b5e059ca 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -4,11 +4,14 @@ from django import forms from taggit.forms import TagField from dcim.models import Device -from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, +) from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField, - StaticSelect2Multiple + StaticSelect2Multiple, TagFilterField ) +from .constants import * from .models import Secret, SecretRole, UserKey @@ -67,9 +70,9 @@ class SecretRoleCSVForm(forms.ModelForm): # Secrets # -class SecretForm(BootstrapMixin, CustomFieldForm): +class SecretForm(BootstrapMixin, CustomFieldModelForm): plaintext = forms.CharField( - max_length=65535, + max_length=SECRET_PLAINTEXT_MAX_LENGTH, required=False, label='Plaintext', widget=forms.PasswordInput( @@ -79,7 +82,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm): ) ) plaintext2 = forms.CharField( - max_length=65535, + max_length=SECRET_PLAINTEXT_MAX_LENGTH, required=False, label='Plaintext (verify)', widget=forms.PasswordInput() @@ -115,7 +118,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm): }) -class SecretCSVForm(forms.ModelForm): +class SecretCSVForm(CustomFieldModelCSVForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -186,6 +189,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) + tag = TagFilterField(model) # diff --git a/netbox/secrets/tests/test_views.py b/netbox/secrets/tests/test_views.py index 43ae10dc6..94f4cbd6a 100644 --- a/netbox/secrets/tests/test_views.py +++ b/netbox/secrets/tests/test_views.py @@ -1,26 +1,23 @@ import base64 -import urllib.parse -from django.test import Client, TestCase from django.urls import reverse from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from secrets.models import Secret, SecretRole, SessionKey, UserKey -from utilities.testing import create_test_user +from utilities.testing import StandardTestCases from .constants import PRIVATE_KEY, PUBLIC_KEY -class SecretRoleTestCase(TestCase): +class SecretRoleTestCase(StandardTestCases.Views): + model = SecretRole - def setUp(self): - user = create_test_user( - permissions=[ - 'secrets.view_secretrole', - 'secrets.add_secretrole', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): SecretRole.objects.bulk_create([ SecretRole(name='Secret Role 1', slug='secret-role-1'), @@ -28,89 +25,83 @@ class SecretRoleTestCase(TestCase): SecretRole(name='Secret Role 3', slug='secret-role-3'), ]) - def test_secretrole_list(self): + cls.form_data = { + 'name': 'Secret Role X', + 'slug': 'secret-role-x', + 'description': 'A secret role', + 'users': [], + 'groups': [], + } - url = reverse('secrets:secretrole_list') - - response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200) - - def test_secretrole_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Secret Role 4,secret-role-4", "Secret Role 5,secret-role-5", "Secret Role 6,secret-role-6", ) - response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(SecretRole.objects.count(), 6) +class SecretTestCase(StandardTestCases.Views): + model = Secret + # Disable inapplicable tests + test_create_object = None -class SecretTestCase(TestCase): + # TODO: Check permissions enforcement on secrets.views.secret_edit + test_edit_object = None + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1') + devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole), + ) + Device.objects.bulk_create(devices) + + secretroles = ( + SecretRole(name='Secret Role 1', slug='secret-role-1'), + SecretRole(name='Secret Role 2', slug='secret-role-2'), + ) + SecretRole.objects.bulk_create(secretroles) + + # Create one secret per device to allow bulk-editing of names (which must be unique per device/role) + Secret.objects.bulk_create(( + Secret(device=devices[0], role=secretroles[0], name='Secret 1', ciphertext=b'1234567890'), + Secret(device=devices[1], role=secretroles[0], name='Secret 2', ciphertext=b'1234567890'), + Secret(device=devices[2], role=secretroles[0], name='Secret 3', ciphertext=b'1234567890'), + )) + + cls.form_data = { + 'device': devices[1].pk, + 'role': secretroles[1].pk, + 'name': 'Secret X', + } + + cls.bulk_edit_data = { + 'role': secretroles[1].pk, + 'name': 'New name', + } def setUp(self): - user = create_test_user( - permissions=[ - 'secrets.view_secret', - 'secrets.add_secret', - ] - ) - # Set up a master key - userkey = UserKey(user=user, public_key=PUBLIC_KEY) + super().setUp() + + # Set up a master key for the test user + userkey = UserKey(user=self.user, public_key=PUBLIC_KEY) userkey.save() master_key = userkey.get_master_key(PRIVATE_KEY) self.session_key = SessionKey(userkey=userkey) self.session_key.save(master_key) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1') - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() - - secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1') - secretrole.save() - - Secret.objects.bulk_create([ - Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'), - Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'), - Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'), - ]) - - def test_secret_list(self): - - url = reverse('secrets:secret_list') - params = { - "role": SecretRole.objects.first().slug, - } - - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True) - self.assertEqual(response.status_code, 200) - - def test_secret(self): - - secret = Secret.objects.first() - response = self.client.get(secret.get_absolute_url(), follow=True) - self.assertEqual(response.status_code, 200) - - def test_secret_import(self): + def test_import_objects(self): + self.add_permissions('secrets.add_secret') csv_data = ( "device,role,name,plaintext", @@ -125,5 +116,5 @@ class SecretTestCase(TestCase): response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Secret.objects.count(), 6) diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index 9d07dd63c..4ed08da7f 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -8,21 +8,21 @@ app_name = 'secrets' urlpatterns = [ # Secret roles - path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'), - path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'), - path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), - path(r'secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), - path(r'secret-roles//edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'), - path(r'secret-roles//changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), + path('secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'), + path('secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'), + path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), + path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), + path('secret-roles//edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'), + path('secret-roles//changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), # Secrets - path(r'secrets/', views.SecretListView.as_view(), name='secret_list'), - path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'), - path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), - path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), - path(r'secrets//', views.SecretView.as_view(), name='secret'), - path(r'secrets//edit/', views.secret_edit, name='secret_edit'), - path(r'secrets//delete/', views.SecretDeleteView.as_view(), name='secret_delete'), - path(r'secrets//changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), + path('secrets/', views.SecretListView.as_view(), name='secret_list'), + path('secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'), + path('secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), + path('secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), + path('secrets//', views.SecretView.as_view(), name='secret'), + path('secrets//edit/', views.secret_edit, name='secret_edit'), + path('secrets//delete/', views.SecretDeleteView.as_view(), name='secret_delete'), + path('secrets//changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), ] diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index d686bdf7a..169aab072 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index e4ee7fb2b..4126f75ec 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 4dd145058..8c7b69f26 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -32,7 +32,7 @@ {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} -

{{ cable.get_status_display }}

+

{{ cable.get_status_display }}

{{ cable.get_type_display|default:"" }}

{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} {% if cable.color %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index fa37f1ac5..5ede19d78 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -48,14 +48,30 @@ Add Components {% endif %} @@ -333,12 +349,12 @@ {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/virtualchassis_list.html b/netbox/templates/dcim/virtualchassis_list.html index 8c26f3c3e..55cfc1691 100644 --- a/netbox/templates/dcim/virtualchassis_list.html +++ b/netbox/templates/dcim/virtualchassis_list.html @@ -13,7 +13,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 7cec3f403..7731acae5 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -1,5 +1,6 @@ {% extends '_base.html' %} {% load helpers %} +{% load static %} {% block header %}
@@ -134,6 +135,34 @@ {% endif %} + + Cluster Groups + + {% if configcontext.cluster_groups.all %} +
    + {% for cluster_group in configcontext.cluster_groups.all %} +
  • {{ cluster_group }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} + + + + Clusters + + {% if configcontext.clusters.all %} +
    + {% for cluster in configcontext.clusters.all %} +
  • {{ cluster }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} + + Tenant Groups @@ -183,11 +212,16 @@
Data + {% include 'extras/inc/configcontext_format.html' %}
-
{{ configcontext.data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=configcontext.data %}
{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/extras/configcontext_edit.html b/netbox/templates/extras/configcontext_edit.html index d31aa5c57..9e922108c 100644 --- a/netbox/templates/extras/configcontext_edit.html +++ b/netbox/templates/extras/configcontext_edit.html @@ -18,6 +18,8 @@ {% render_field form.sites %} {% render_field form.roles %} {% render_field form.platforms %} + {% render_field form.cluster_groups %} + {% render_field form.clusters %} {% render_field form.tenant_groups %} {% render_field form.tenants %} {% render_field form.tags %} diff --git a/netbox/templates/extras/inc/configcontext_data.html b/netbox/templates/extras/inc/configcontext_data.html new file mode 100644 index 000000000..d91960e2c --- /dev/null +++ b/netbox/templates/extras/inc/configcontext_data.html @@ -0,0 +1,8 @@ +{% load helpers %} + +
+
{{ data|render_json }}
+
+ diff --git a/netbox/templates/extras/inc/configcontext_format.html b/netbox/templates/extras/inc/configcontext_format.html new file mode 100644 index 000000000..d3467f131 --- /dev/null +++ b/netbox/templates/extras/inc/configcontext_format.html @@ -0,0 +1,6 @@ +
+
+ JSON + YAML +
+
diff --git a/netbox/templates/extras/object_configcontext.html b/netbox/templates/extras/object_configcontext.html index d23455c19..784b5805f 100644 --- a/netbox/templates/extras/object_configcontext.html +++ b/netbox/templates/extras/object_configcontext.html @@ -1,5 +1,6 @@ {% extends base_template %} {% load helpers %} +{% load static %} {% block title %}{{ block.super }} - Config Context{% endblock %} @@ -9,9 +10,10 @@
Rendered Context + {% include 'extras/inc/configcontext_format.html' %}
-
{{ rendered_context|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=rendered_context %}
@@ -22,7 +24,7 @@
{% if obj.local_context_data %} -
{{ obj.local_context_data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=obj.local_context_data %} {% else %} None {% endif %} @@ -47,7 +49,7 @@ {% if context.description %}
{{ context.description }} {% endif %} -
{{ context.data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=context.data %}
{% empty %}
@@ -58,3 +60,7 @@
{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html index 52d9c2d6e..84892f726 100644 --- a/netbox/templates/inc/custom_fields_panel.html +++ b/netbox/templates/inc/custom_fields_panel.html @@ -9,13 +9,13 @@ {{ field }} - {% if field.type == 300 and value == True %} + {% if field.type == 'boolean' and value == True %} - {% elif field.type == 300 and value == False %} + {% elif field.type == 'boolean' and value == False %} - {% elif field.type == 500 and value %} + {% elif field.type == 'url' and value %} {{ value|truncatechars:70 }} - {% elif field.type == 200 or value %} + {% elif field.type == 'integer' or value %} {{ value }} {% elif field.required %} Not defined diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html deleted file mode 100644 index a7923fbed..000000000 --- a/netbox/templates/inc/tags_panel.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load helpers %} - -
-
- - Tags -
-
- {% for tag in tags %} - {{ tag }} {{ tag.count }} - {% endfor %} -
-
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index aad747b2d..27363a56d 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -17,7 +17,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
Statistics diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index c24c94c87..e3f694fe3 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -61,7 +61,7 @@ {% render_field form.nat_device %}
{% render_field form.nat_inside %} diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index 12f227301..b7920a434 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index b80af8e1d..f0754d37b 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -21,7 +21,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/service_list.html b/netbox/templates/ipam/service_list.html index a39bec22e..4aac520d9 100644 --- a/netbox/templates/ipam/service_list.html +++ b/netbox/templates/ipam/service_list.html @@ -12,7 +12,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index b4d313a8c..24d538f88 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 566e2f3e6..975c73a37 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index b6d792765..ee631b439 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -15,7 +15,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index 91463c52c..a77636a5b 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/cluster_edit.html b/netbox/templates/virtualization/cluster_edit.html index bf81ffd42..c4d39d12e 100644 --- a/netbox/templates/virtualization/cluster_edit.html +++ b/netbox/templates/virtualization/cluster_edit.html @@ -8,7 +8,6 @@ {% render_field form.name %} {% render_field form.type %} {% render_field form.group %} - {% render_field form.tenant %} {% render_field form.site %} diff --git a/netbox/templates/virtualization/cluster_list.html b/netbox/templates/virtualization/cluster_list.html index 3fef90c03..6f5f058ad 100644 --- a/netbox/templates/virtualization/cluster_list.html +++ b/netbox/templates/virtualization/cluster_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 8cf1fd490..10c1f36d4 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -265,7 +265,9 @@ Name LAG Description + MTU Mode + Cable Connection @@ -286,18 +288,18 @@ - {% endif %} {% if interfaces and perms.dcim.delete_interface %} - {% endif %} {% if perms.dcim.add_interface %} diff --git a/netbox/templates/virtualization/virtualmachine_component_add.html b/netbox/templates/virtualization/virtualmachine_component_add.html index 7cac56705..90754519b 100644 --- a/netbox/templates/virtualization/virtualmachine_component_add.html +++ b/netbox/templates/virtualization/virtualmachine_component_add.html @@ -5,7 +5,7 @@ {% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} {% block content %} - + {% csrf_token %}
diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index b10341547..821f956a2 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index 3da0e0f82..5762f9a0d 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -15,11 +15,11 @@ router = routers.DefaultRouter() router.APIRootView = TenancyRootView # Field choices -router.register(r'_choices', views.TenancyFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.TenancyFieldChoicesViewSet, basename='field-choice') # Tenants -router.register(r'tenant-groups', views.TenantGroupViewSet) -router.register(r'tenants', views.TenantViewSet) +router.register('tenant-groups', views.TenantGroupViewSet) +router.register('tenants', views.TenantViewSet) app_name = 'tenancy-api' urlpatterns = router.urls diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index f8aaa45e5..b0468b37a 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -1,10 +1,12 @@ from django import forms from taggit.forms import TagField -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, +) from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, - FilterChoiceField, SlugField, + FilterChoiceField, SlugField, TagFilterField ) from .models import Tenant, TenantGroup @@ -38,7 +40,7 @@ class TenantGroupCSVForm(forms.ModelForm): # Tenants # -class TenantForm(BootstrapMixin, CustomFieldForm): +class TenantForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() comments = CommentField() tags = TagField( @@ -57,7 +59,7 @@ class TenantForm(BootstrapMixin, CustomFieldForm): } -class TenantCSVForm(forms.ModelForm): +class TenantCSVForm(CustomFieldModelForm): slug = SlugField() group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), @@ -113,6 +115,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 10ee354d4..a44ca2932 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -1,23 +1,17 @@ -import urllib.parse - -from django.test import Client, TestCase -from django.urls import reverse - from tenancy.models import Tenant, TenantGroup -from utilities.testing import create_test_user +from utilities.testing import StandardTestCases -class TenantGroupTestCase(TestCase): +class TenantGroupTestCase(StandardTestCases.Views): + model = TenantGroup - def setUp(self): - user = create_test_user( - permissions=[ - 'tenancy.view_tenantgroup', - 'tenancy.add_tenantgroup', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): TenantGroup.objects.bulk_create([ TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), @@ -25,75 +19,53 @@ class TenantGroupTestCase(TestCase): TenantGroup(name='Tenant Group 3', slug='tenant-group-3'), ]) - def test_tenantgroup_list(self): + cls.form_data = { + 'name': 'Tenant Group X', + 'slug': 'tenant-group-x', + } - url = reverse('tenancy:tenantgroup_list') - - response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200) - - def test_tenantgroup_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Tenant Group 4,tenant-group-4", "Tenant Group 5,tenant-group-5", "Tenant Group 6,tenant-group-6", ) - response = self.client.post(reverse('tenancy:tenantgroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(TenantGroup.objects.count(), 6) +class TenantTestCase(StandardTestCases.Views): + model = Tenant + @classmethod + def setUpTestData(cls): -class TenantTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'tenancy.view_tenant', - 'tenancy.add_tenant', - ] + tenantgroups = ( + TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant Group 2', slug='tenant-group-2'), ) - self.client = Client() - self.client.force_login(user) - - tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1') - tenantgroup.save() + TenantGroup.objects.bulk_create(tenantgroups) Tenant.objects.bulk_create([ - Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup), - Tenant(name='Tenant 2', slug='tenant-2', group=tenantgroup), - Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup), + Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenantgroups[0]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroups[0]), ]) - def test_tenant_list(self): - - url = reverse('tenancy:tenant_list') - params = { - "group": TenantGroup.objects.first().slug, + cls.form_data = { + 'name': 'Tenant X', + 'slug': 'tenant-x', + 'group': tenantgroups[1].pk, + 'description': 'A new tenant', + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True) - self.assertEqual(response.status_code, 200) - - def test_tenant(self): - - tenant = Tenant.objects.first() - response = self.client.get(tenant.get_absolute_url(), follow=True) - self.assertEqual(response.status_code, 200) - - def test_tenant_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Tenant 4,tenant-4", "Tenant 5,tenant-5", "Tenant 6,tenant-6", ) - response = self.client.post(reverse('tenancy:tenant_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Tenant.objects.count(), 6) + cls.bulk_edit_data = { + 'group': tenantgroups[1].pk, + } diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index fb23a6ef1..0218a5674 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -8,22 +8,22 @@ app_name = 'tenancy' urlpatterns = [ # Tenant groups - path(r'tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'), - path(r'tenant-groups/add/', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'), - path(r'tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), - path(r'tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), - path(r'tenant-groups//edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), - path(r'tenant-groups//changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), + path('tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'), + path('tenant-groups/add/', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'), + path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), + path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), + path('tenant-groups//edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), + path('tenant-groups//changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), # Tenants - path(r'tenants/', views.TenantListView.as_view(), name='tenant_list'), - path(r'tenants/add/', views.TenantCreateView.as_view(), name='tenant_add'), - path(r'tenants/import/', views.TenantBulkImportView.as_view(), name='tenant_import'), - path(r'tenants/edit/', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), - path(r'tenants/delete/', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), - path(r'tenants//', views.TenantView.as_view(), name='tenant'), - path(r'tenants//edit/', views.TenantEditView.as_view(), name='tenant_edit'), - path(r'tenants//delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), - path(r'tenants//changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), + path('tenants/', views.TenantListView.as_view(), name='tenant_list'), + path('tenants/add/', views.TenantCreateView.as_view(), name='tenant_add'), + path('tenants/import/', views.TenantBulkImportView.as_view(), name='tenant_import'), + path('tenants/edit/', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), + path('tenants/delete/', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), + path('tenants//', views.TenantView.as_view(), name='tenant'), + path('tenants//edit/', views.TenantEditView.as_view(), name='tenant_edit'), + path('tenants//delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), + path('tenants//changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), ] diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 40fdbeab1..dae540726 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -5,14 +5,14 @@ from . import views app_name = 'user' urlpatterns = [ - path(r'profile/', views.ProfileView.as_view(), name='profile'), - path(r'password/', views.ChangePasswordView.as_view(), name='change_password'), - path(r'api-tokens/', views.TokenListView.as_view(), name='token_list'), - path(r'api-tokens/add/', views.TokenEditView.as_view(), name='token_add'), - path(r'api-tokens//edit/', views.TokenEditView.as_view(), name='token_edit'), - path(r'api-tokens//delete/', views.TokenDeleteView.as_view(), name='token_delete'), - path(r'user-key/', views.UserKeyView.as_view(), name='userkey'), - path(r'user-key/edit/', views.UserKeyEditView.as_view(), name='userkey_edit'), - path(r'session-key/delete/', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'), + path('profile/', views.ProfileView.as_view(), name='profile'), + path('password/', views.ChangePasswordView.as_view(), name='change_password'), + path('api-tokens/', views.TokenListView.as_view(), name='token_list'), + path('api-tokens/add/', views.TokenEditView.as_view(), name='token_add'), + path('api-tokens//edit/', views.TokenEditView.as_view(), name='token_edit'), + path('api-tokens//delete/', views.TokenDeleteView.as_view(), name='token_delete'), + path('user-key/', views.UserKeyView.as_view(), name='userkey'), + path('user-key/edit/', views.UserKeyEditView.as_view(), name='userkey_edit'), + path('session-key/delete/', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'), ] diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 7c37a5b20..5ef4156aa 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -13,7 +13,6 @@ from rest_framework.response import Response from rest_framework.serializers import Field, ModelSerializer, ValidationError from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet -from utilities.choices import ChoiceSet from .utils import dict_to_filter_params, dynamic_import diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py index 6e6afe3e2..19082dbb6 100644 --- a/netbox/utilities/choices.py +++ b/netbox/utilities/choices.py @@ -18,7 +18,7 @@ class ChoiceSet(metaclass=ChoiceSetMeta): @classmethod def values(cls): - return [c[0] for c in cls.CHOICES] + return [c[0] for c in unpack_grouped_choices(cls.CHOICES)] @classmethod def as_dict(cls): diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index a14ec9305..355484673 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -7,6 +7,7 @@ import yaml from django import forms from django.conf import settings from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput +from django.db.models import Count from mptt.forms import TreeNodeMultipleChoiceField from .choices import unpack_grouped_choices @@ -450,12 +451,14 @@ class ExpandableNameField(forms.CharField): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.help_text: - self.help_text = 'Alphanumeric ranges are supported for bulk creation.
' \ - 'Mixed cases and types within a single range are not supported.
' \ - 'Examples:
  • ge-0/0/[0-23,25,30]
  • ' \ - '
  • e[0-3][a-d,f]
  • ' \ - '
  • [xe,ge]-0/0/0
  • ' \ - '
  • e[0-3,a-d,f]
' + self.help_text = """ + Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range + are not supported. Examples: +
    +
  • [ge,xe]-0/0/[0-9]
  • +
  • e[0-3][a-d,f]
  • +
+ """ def to_python(self, value): if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value): @@ -561,6 +564,23 @@ class SlugField(forms.SlugField): self.widget.attrs['slug-source'] = slug_source +class TagFilterField(forms.MultipleChoiceField): + """ + A filter field for the tags of a model. Only the tags used by a model are displayed. + + :param model: The model of the filter + """ + widget = StaticSelect2Multiple + + def __init__(self, model, *args, **kwargs): + def get_choices(): + tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') + return [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] + + # Choices are fetched each time the form is initialized + super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs) + + class FilterChoiceIterator(forms.models.ModelChoiceIterator): def __iter__(self): @@ -709,26 +729,13 @@ class ConfirmationForm(BootstrapMixin, ReturnURLForm): confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True) -class ComponentForm(BootstrapMixin, forms.Form): - """ - Allow inclusion of the parent Device/VirtualMachine as context for limiting field choices. - """ - def __init__(self, parent, *args, **kwargs): - self.parent = parent - super().__init__(*args, **kwargs) - - def get_iterative_data(self, iteration): - return {} - - class BulkEditForm(forms.Form): """ Base form for editing multiple objects in bulk """ - def __init__(self, model, parent_obj=None, *args, **kwargs): + def __init__(self, model, *args, **kwargs): super().__init__(*args, **kwargs) self.model = model - self.parent_obj = parent_obj self.nullable_fields = [] # Copy any nullable fields defined in Meta diff --git a/netbox/utilities/management/commands/makemigrations.py b/netbox/utilities/management/commands/makemigrations.py index fbcf82eaf..69f699796 100644 --- a/netbox/utilities/management/commands/makemigrations.py +++ b/netbox/utilities/management/commands/makemigrations.py @@ -1,7 +1,28 @@ # noinspection PyUnresolvedReferences -from django.core.management.commands.makemigrations import Command +from django.conf import settings +from django.core.management.base import CommandError +from django.core.management.commands.makemigrations import Command as _Command from django.db import models from . import custom_deconstruct models.Field.deconstruct = custom_deconstruct + + +class Command(_Command): + + def handle(self, *args, **kwargs): + """ + This built-in management command enables the creation of new database schema migration files, which should + never be required by and ordinary user. We prevent this command from executing unless the configuration + indicates that the user is a developer (i.e. configuration.DEVELOPER == True). + """ + if not settings.DEVELOPER: + raise CommandError( + "This command is available for development purposes only. It will\n" + "NOT resolve any issues with missing or unapplied migrations. For assistance,\n" + "please post to the NetBox mailing list:\n" + " https://groups.google.com/forum/#!forum/netbox-discuss" + ) + + super().handle(*args, **kwargs) diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index a44273ab0..564771821 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -7,9 +7,6 @@ from django.urls import reverse from .views import server_error -BASE_PATH = getattr(settings, 'BASE_PATH', False) -LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False) - class LoginRequiredMiddleware(object): """ @@ -19,7 +16,7 @@ class LoginRequiredMiddleware(object): self.get_response = get_response def __call__(self, request): - if LOGIN_REQUIRED and not request.user.is_authenticated: + if settings.LOGIN_REQUIRED and not request.user.is_authenticated: # Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API # performs its own authentication. Also metrics can be read without login. api_path = reverse('api-root') diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index c4b3bb6ea..4278b3b95 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,6 +1,7 @@ import datetime import json import re +import yaml from django import template from django.utils.html import strip_tags @@ -76,6 +77,14 @@ def render_json(value): return json.dumps(value, indent=4, sort_keys=True) +@register.filter() +def render_yaml(value): + """ + Render a dictionary as formatted YAML. + """ + return yaml.dump(dict(value)) + + @register.filter() def model_name(obj): """ diff --git a/netbox/utilities/testing/__init__.py b/netbox/utilities/testing/__init__.py new file mode 100644 index 000000000..30e452215 --- /dev/null +++ b/netbox/utilities/testing/__init__.py @@ -0,0 +1,2 @@ +from .testcases import * +from .utils import * diff --git a/netbox/utilities/testing/testcases.py b/netbox/utilities/testing/testcases.py new file mode 100644 index 000000000..b5e2e1bab --- /dev/null +++ b/netbox/utilities/testing/testcases.py @@ -0,0 +1,396 @@ +from django.contrib.auth.models import Permission, User +from django.core.exceptions import ObjectDoesNotExist +from django.forms.models import model_to_dict +from django.test import Client, TestCase as _TestCase, override_settings +from django.urls import reverse, NoReverseMatch +from rest_framework.test import APIClient + +from users.models import Token +from .utils import disable_warnings, post_data + + +class TestCase(_TestCase): + user_permissions = () + + def setUp(self): + + # Create the test user and assign permissions + self.user = User.objects.create_user(username='testuser') + self.add_permissions(*self.user_permissions) + + # Initialize the test client + self.client = Client() + self.client.force_login(self.user) + + # + # Permissions management + # + + def add_permissions(self, *names): + """ + Assign a set of permissions to the test user. Accepts permission names in the form ._. + """ + for name in names: + app, codename = name.split('.') + perm = Permission.objects.get(content_type__app_label=app, codename=codename) + self.user.user_permissions.add(perm) + + def remove_permissions(self, *names): + """ + Remove a set of permissions from the test user, if assigned. + """ + for name in names: + app, codename = name.split('.') + perm = Permission.objects.get(content_type__app_label=app, codename=codename) + self.user.user_permissions.remove(perm) + + # + # Convenience methods + # + + def assertHttpStatus(self, response, expected_status): + """ + TestCase method. Provide more detail in the event of an unexpected HTTP response. + """ + err_message = "Expected HTTP status {}; received {}: {}" + self.assertEqual(response.status_code, expected_status, err_message.format( + expected_status, response.status_code, getattr(response, 'data', 'No data') + )) + + def assertInstanceEqual(self, instance, data): + """ + Compare a model instance to a dictionary, checking that its attribute values match those specified + in the dictionary. + """ + model_dict = model_to_dict(instance, fields=data.keys()) + + for key in list(model_dict.keys()): + + # TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext) + if key == 'tags': + model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']])) + + # Convert ManyToManyField to list of instance PKs + elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'): + model_dict[key] = [obj.pk for obj in model_dict[key]] + + # Omit any dictionary keys which are not instance attributes + relevant_data = { + k: v for k, v in data.items() if hasattr(instance, k) + } + + self.assertDictEqual(model_dict, relevant_data) + + +class APITestCase(TestCase): + client_class = APIClient + + def setUp(self): + """ + Create a superuser and token for API calls. + """ + self.user = User.objects.create(username='testuser', is_superuser=True) + self.token = Token.objects.create(user=self.user) + self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)} + + +class StandardTestCases: + """ + We keep any TestCases with test_* methods inside a class to prevent unittest from trying to run them. + """ + + class Views(TestCase): + """ + Stock TestCase suitable for testing all standard View functions: + - List objects + - View single object + - Create new object + - Modify existing object + - Delete existing object + - Import multiple new objects + """ + model = None + + # Data to be sent when creating/editing individual objects + form_data = {} + + # CSV lines used for bulk import of new objects + csv_data = () + + # Form data used when creating multiple objects + bulk_create_data = {} + + # Form data to be used when editing multiple objects at once + bulk_edit_data = {} + + maxDiff = None + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + if self.model is None: + raise Exception("Test case requires model to be defined") + + # + # URL functions + # + + def _get_base_url(self): + """ + Return the base format for a URL for the test's model. Override this to test for a model which belongs + to a different app (e.g. testing Interfaces within the virtualization app). + """ + return '{}:{}_{{}}'.format( + self.model._meta.app_label, + self.model._meta.model_name + ) + + def _get_url(self, action, instance=None): + """ + Return the URL name for a specific action. An instance must be specified for + get/edit/delete views. + """ + url_format = self._get_base_url() + + if action in ('list', 'add', 'import', 'bulk_edit', 'bulk_delete'): + return reverse(url_format.format(action)) + + elif action in ('get', 'edit', 'delete'): + if instance is None: + raise Exception("Resolving {} URL requires specifying an instance".format(action)) + # Attempt to resolve using slug first + if hasattr(self.model, 'slug'): + try: + return reverse(url_format.format(action), kwargs={'slug': instance.slug}) + except NoReverseMatch: + pass + return reverse(url_format.format(action), kwargs={'pk': instance.pk}) + + else: + raise Exception("Invalid action for URL resolution: {}".format(action)) + + # + # Standard view tests + # These methods will run by default. To disable a test, nullify its method on the subclasses TestCase: + # + # test_list_objects = None + # + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_list_objects(self): + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.get(self._get_url('list')), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.get(self._get_url('list')) + self.assertHttpStatus(response, 200) + + # Built-in CSV export + if hasattr(self.model, 'csv_headers'): + response = self.client.get('{}?export'.format(self._get_url('list'))) + self.assertHttpStatus(response, 200) + self.assertEqual(response.get('Content-Type'), 'text/csv') + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_get_object(self): + instance = self.model.objects.first() + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.get(instance.get_absolute_url()) + self.assertHttpStatus(response, 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_create_object(self): + initial_count = self.model.objects.count() + request = { + 'path': self._get_url('add'), + 'data': post_data(self.form_data), + 'follow': False, # Do not follow 302 redirects + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + self.assertEqual(initial_count + 1, self.model.objects.count()) + instance = self.model.objects.order_by('-pk').first() + self.assertInstanceEqual(instance, self.form_data) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_edit_object(self): + instance = self.model.objects.first() + + request = { + 'path': self._get_url('edit', instance), + 'data': post_data(self.form_data), + 'follow': False, # Do not follow 302 redirects + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + instance = self.model.objects.get(pk=instance.pk) + self.assertInstanceEqual(instance, self.form_data) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_delete_object(self): + instance = self.model.objects.first() + + request = { + 'path': self._get_url('delete', instance), + 'data': {'confirm': True}, + 'follow': False, # Do not follow 302 redirects + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.delete_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + with self.assertRaises(ObjectDoesNotExist): + self.model.objects.get(pk=instance.pk) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_import_objects(self): + initial_count = self.model.objects.count() + request = { + 'path': self._get_url('import'), + 'data': { + 'csv': '\n'.join(self.csv_data) + } + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name), + '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 200) + + self.assertEqual(self.model.objects.count(), initial_count + len(self.csv_data) - 1) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_bulk_edit_objects(self): + # Bulk edit the first three objects only + pk_list = self.model.objects.values_list('pk', flat=True)[:3] + + request = { + 'path': self._get_url('bulk_edit'), + 'data': { + 'pk': pk_list, + '_apply': True, # Form button + }, + 'follow': False, # Do not follow 302 redirects + } + + # Append the form data to the request + request['data'].update(post_data(self.bulk_edit_data)) + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + for i, instance in enumerate(self.model.objects.filter(pk__in=pk_list)): + self.assertInstanceEqual(instance, self.bulk_edit_data) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def test_bulk_delete_objects(self): + pk_list = self.model.objects.values_list('pk', flat=True) + + request = { + 'path': self._get_url('bulk_delete'), + 'data': { + 'pk': pk_list, + 'confirm': True, + '_confirm': True, # Form button + }, + 'follow': False, # Do not follow 302 redirects + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.delete_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + # Check that all objects were deleted + self.assertEqual(self.model.objects.count(), 0) + + # + # Optional view tests + # These methods will run only if the required data + # + + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) + def _test_bulk_create_objects(self, expected_count): + initial_count = self.model.objects.count() + request = { + 'path': self._get_url('add'), + 'data': post_data(self.bulk_create_data), + 'follow': False, # Do not follow 302 redirects + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions( + '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name) + ) + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + + self.assertEqual(initial_count + expected_count, self.model.objects.count()) + for instance in self.model.objects.order_by('-pk')[:expected_count]: + self.assertInstanceEqual(instance, self.bulk_create_data) diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing/utils.py similarity index 62% rename from netbox/utilities/testing.py rename to netbox/utilities/testing/utils.py index 791eb64cb..469b21111 100644 --- a/netbox/utilities/testing.py +++ b/netbox/utilities/testing/utils.py @@ -2,29 +2,23 @@ import logging from contextlib import contextmanager from django.contrib.auth.models import Permission, User -from rest_framework.test import APITestCase as _APITestCase - -from users.models import Token -class APITestCase(_APITestCase): +def post_data(data): + """ + Take a dictionary of test data (suitable for comparison to an instance) and return a dict suitable for POSTing. + """ + ret = {} - def setUp(self): - """ - Create a superuser and token for API calls. - """ - self.user = User.objects.create(username='testuser', is_superuser=True) - self.token = Token.objects.create(user=self.user) - self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)} + for key, value in data.items(): + if value is None: + ret[key] = '' + elif type(value) in (list, tuple): + ret[key] = value + else: + ret[key] = str(value) - def assertHttpStatus(self, response, expected_status): - """ - Provide more detail in the event of an unexpected HTTP response. - """ - err_message = "Expected HTTP status {}; received {}: {}" - self.assertEqual(response.status_code, expected_status, err_message.format( - expected_status, response.status_code, getattr(response, 'data', 'No data') - )) + return ret def create_test_user(username='testuser', permissions=list()): diff --git a/netbox/utilities/tests/test_choices.py b/netbox/utilities/tests/test_choices.py new file mode 100644 index 000000000..adff4eb60 --- /dev/null +++ b/netbox/utilities/tests/test_choices.py @@ -0,0 +1,50 @@ +from django.test import TestCase + +from utilities.choices import ChoiceSet + + +class ExampleChoices(ChoiceSet): + + CHOICE_A = 'a' + CHOICE_B = 'b' + CHOICE_C = 'c' + CHOICE_1 = 1 + CHOICE_2 = 2 + CHOICE_3 = 3 + CHOICES = ( + ('Letters', ( + (CHOICE_A, 'A'), + (CHOICE_B, 'B'), + (CHOICE_C, 'C'), + )), + ('Digits', ( + (CHOICE_1, 'One'), + (CHOICE_2, 'Two'), + (CHOICE_3, 'Three'), + )), + ) + LEGACY_MAP = { + CHOICE_A: 101, + CHOICE_B: 102, + CHOICE_C: 103, + CHOICE_1: 201, + CHOICE_2: 202, + CHOICE_3: 203, + } + + +class ChoiceSetTestCase(TestCase): + + def test_values(self): + self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3]) + + def test_as_dict(self): + self.assertEqual(ExampleChoices.as_dict(), { + 'a': 'A', 'b': 'B', 'c': 'C', 1: 'One', 2: 'Two', 3: 'Three' + }) + + def test_slug_to_id(self): + self.assertEqual(ExampleChoices.slug_to_id('a'), 101) + + def test_id_to_slug(self): + self.assertEqual(ExampleChoices.id_to_slug(101), 'a') diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index dc2185988..979f95af9 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -4,6 +4,7 @@ from collections import OrderedDict from django.core.serializers import serialize from django.db.models import Count, OuterRef, Subquery +from django.http import QueryDict from jinja2 import Environment from dcim.choices import CableLengthUnitChoices @@ -209,3 +210,15 @@ def prepare_cloned_fields(instance): ) return param_string + + +def querydict_to_dict(querydict): + """ + Convert a django.http.QueryDict object to a regular Python dictionary, preserving lists of multiple values. + (QueryDict.dict() will return only the last value in a list for each key.) + """ + assert isinstance(querydict, QueryDict) + return { + key: querydict.get(key) if len(value) == 1 and key != 'pk' else querydict.getlist(key) + for key, value in querydict.lists() + } diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 5cb81c6e6..7c38aceee 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -4,11 +4,10 @@ from copy import deepcopy from django.conf import settings from django.contrib import messages from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.db import transaction, IntegrityError -from django.db.models import Count, ProtectedError -from django.db.models.query import QuerySet -from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea +from django.db.models import ManyToManyField, ProtectedError +from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render from django.template import loader @@ -24,10 +23,9 @@ from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset -from extras.utils import is_taggable from utilities.exceptions import AbortTransaction from utilities.forms import BootstrapMixin, CSVDataField -from utilities.utils import csv_format, prepare_cloned_fields +from utilities.utils import csv_format, prepare_cloned_fields, querydict_to_dict from .error_handlers import handle_protectederror from .forms import ConfirmationForm, ImportForm from .paginator import EnhancedPaginator @@ -88,15 +86,27 @@ class ObjectListView(View): Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method. """ csv_data = [] + custom_fields = [] # Start with the column headers - headers = ','.join(self.queryset.model.csv_headers) - csv_data.append(headers) + headers = self.queryset.model.csv_headers.copy() + + # Add custom field headers, if any + if hasattr(self.queryset.model, 'get_custom_fields'): + for custom_field in self.queryset.model().get_custom_fields(): + headers.append(custom_field.name) + custom_fields.append(custom_field.name) + + csv_data.append(','.join(headers)) # Iterate through the queryset appending each object for obj in self.queryset: - data = csv_format(obj.to_csv()) - csv_data.append(data) + data = obj.to_csv() + + for custom_field in custom_fields: + data += (obj.cf.get(custom_field, ''),) + + csv_data.append(csv_format(data)) return '\n'.join(csv_data) @@ -155,12 +165,6 @@ class ObjectListView(View): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') - # Construct queryset for tags list - if is_taggable(model): - tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') - else: - tags = None - # Apply the request context paginate = { 'paginator_class': EnhancedPaginator, @@ -173,7 +177,6 @@ class ObjectListView(View): 'table': table, 'permissions': permissions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, - 'tags': tags, } context.update(self.extra_context()) @@ -601,14 +604,12 @@ class BulkEditView(GetReturnURLMixin, View): Edit objects in bulk. queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) - parent_model: The model of the parent object (if any) filter: FilterSet to apply when deleting by QuerySet table: The table used to display devices being edited form: The form class used to edit objects in bulk template_name: The name of the template """ queryset = None - parent_model = None filterset = None table = None form = None @@ -621,24 +622,21 @@ class BulkEditView(GetReturnURLMixin, View): model = self.queryset.model - # Attempt to derive parent object if a parent class has been given - if self.parent_model: - parent_obj = get_object_or_404(self.parent_model, **kwargs) - else: - parent_obj = None + # Create a mutable copy of the POST data + post_data = request.POST.copy() - # Are we editing *all* objects in the queryset or just a selected subset? - if request.POST.get('_all') and self.filterset is not None: - pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] - else: - pk_list = [int(pk) for pk in request.POST.getlist('pk')] + # If we are editing *all* objects in the queryset, replace the PK list with all matched objects. + if post_data.get('_all') and self.filterset is not None: + post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] if '_apply' in request.POST: - form = self.form(model, parent_obj, request.POST) + form = self.form(model, request.POST, initial=request.GET) if form.is_valid(): custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] - standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk'] + standard_fields = [ + field for field in form.fields if field not in custom_fields + ['pk'] + ] nullified_fields = request.POST.getlist('_nullify') try: @@ -646,18 +644,33 @@ class BulkEditView(GetReturnURLMixin, View): with transaction.atomic(): updated_count = 0 - for obj in model.objects.filter(pk__in=pk_list): + for obj in model.objects.filter(pk__in=form.cleaned_data['pk']): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: - if name in form.nullable_fields and name in nullified_fields and isinstance(form.cleaned_data[name], QuerySet): - getattr(obj, name).set([]) - elif name in form.nullable_fields and name in nullified_fields: - setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None) - elif isinstance(form.cleaned_data[name], QuerySet) and form.cleaned_data[name]: + + try: + model_field = model._meta.get_field(name) + except FieldDoesNotExist: + # The form field is used to modify a field rather than set its value directly, + # so we skip it. + continue + + # Handle nullification + if name in form.nullable_fields and name in nullified_fields: + if isinstance(model_field, ManyToManyField): + getattr(obj, name).set([]) + else: + setattr(obj, name, None if model_field.null else '') + + # ManyToManyFields + elif isinstance(model_field, ManyToManyField): getattr(obj, name).set(form.cleaned_data[name]) - elif form.cleaned_data[name] not in (None, '') and not isinstance(form.cleaned_data[name], QuerySet): + + # Normal fields + elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) + obj.full_clean() obj.save() @@ -699,12 +712,16 @@ class BulkEditView(GetReturnURLMixin, View): messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: - initial_data = request.POST.copy() - initial_data['pk'] = pk_list - form = self.form(model, parent_obj, initial=initial_data) + # Pass the PK list as initial data to avoid binding the form + initial_data = querydict_to_dict(post_data) + + # Append any normal initial data (passed as GET parameters) + initial_data.update(request.GET) + + form = self.form(model, initial=initial_data) # Retrieve objects being edited - table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) + table = self.table(self.queryset.filter(pk__in=post_data.getlist('pk')), orderable=False) if not table.rows: messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural)) return redirect(self.get_return_url(request)) @@ -722,14 +739,12 @@ class BulkDeleteView(GetReturnURLMixin, View): Delete objects in bulk. queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) - parent_model: The model of the parent object (if any) filter: FilterSet to apply when deleting by QuerySet table: The table used to display devices being deleted form: The form class used to delete objects in bulk template_name: The name of the template """ queryset = None - parent_model = None filterset = None table = None form = None @@ -742,12 +757,6 @@ class BulkDeleteView(GetReturnURLMixin, View): model = self.queryset.model - # Attempt to derive parent object if a parent class has been given - if self.parent_model: - parent_obj = get_object_or_404(self.parent_model, **kwargs) - else: - parent_obj = None - # Are we deleting *all* objects in the queryset or just a selected subset? if request.POST.get('_all'): if self.filterset is not None: @@ -789,7 +798,6 @@ class BulkDeleteView(GetReturnURLMixin, View): return render(request, self.template_name, { 'form': form, - 'parent_obj': parent_obj, 'obj_type_plural': model._meta.verbose_name_plural, 'table': table, 'return_url': self.get_return_url(request), @@ -812,45 +820,40 @@ class BulkDeleteView(GetReturnURLMixin, View): # Device/VirtualMachine components # -class ComponentCreateView(View): +# TODO: Replace with BulkCreateView +class ComponentCreateView(GetReturnURLMixin, View): """ Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine. """ - parent_model = None - parent_field = None model = None form = None model_form = None template_name = None - def get(self, request, pk): + def get(self, request): - parent = get_object_or_404(self.parent_model, pk=pk) - form = self.form(parent, initial=request.GET) + form = self.form(initial=request.GET) return render(request, self.template_name, { - 'parent': parent, 'component_type': self.model._meta.verbose_name, 'form': form, - 'return_url': parent.get_absolute_url(), + 'return_url': self.get_return_url(request), }) - def post(self, request, pk): + def post(self, request): - parent = get_object_or_404(self.parent_model, pk=pk) - - form = self.form(parent, request.POST) + form = self.form(request.POST, initial=request.GET) if form.is_valid(): new_components = [] data = deepcopy(request.POST) - data[self.parent_field] = parent.pk for i, name in enumerate(form.cleaned_data['name_pattern']): # Initialize the individual component form data['name'] = name - data.update(form.get_iterative_data(i)) + if hasattr(form, 'get_iterative_data'): + data.update(form.get_iterative_data(i)) component_form = self.model_form(data) if component_form.is_valid(): @@ -869,19 +872,18 @@ class ComponentCreateView(View): for component_form in new_components: component_form.save() - messages.success(request, "Added {} {} to {}.".format( - len(new_components), self.model._meta.verbose_name_plural, parent + messages.success(request, "Added {} {}".format( + len(new_components), self.model._meta.verbose_name_plural )) if '_addanother' in request.POST: - return redirect(request.path) + return redirect(request.get_full_path()) else: - return redirect(parent.get_absolute_url()) + return redirect(self.get_return_url(request)) return render(request, self.template_name, { - 'parent': parent, 'component_type': self.model._meta.verbose_name, 'form': form, - 'return_url': parent.get_absolute_url(), + 'return_url': self.get_return_url(request), }) diff --git a/netbox/virtualization/api/urls.py b/netbox/virtualization/api/urls.py index b27e5be3d..a94e043b2 100644 --- a/netbox/virtualization/api/urls.py +++ b/netbox/virtualization/api/urls.py @@ -15,16 +15,16 @@ router = routers.DefaultRouter() router.APIRootView = VirtualizationRootView # Field choices -router.register(r'_choices', views.VirtualizationFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.VirtualizationFieldChoicesViewSet, basename='field-choice') # Clusters -router.register(r'cluster-types', views.ClusterTypeViewSet) -router.register(r'cluster-groups', views.ClusterGroupViewSet) -router.register(r'clusters', views.ClusterViewSet) +router.register('cluster-types', views.ClusterTypeViewSet) +router.register('cluster-groups', views.ClusterGroupViewSet) +router.register('clusters', views.ClusterViewSet) # VirtualMachines -router.register(r'virtual-machines', views.VirtualMachineViewSet) -router.register(r'interfaces', views.InterfaceViewSet) +router.register('virtual-machines', views.VirtualMachineViewSet) +router.register('interfaces', views.InterfaceViewSet) app_name = 'virtualization-api' urlpatterns = router.urls diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 910e9a39f..1560a683f 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -3,17 +3,20 @@ from django.core.exceptions import ValidationError from taggit.forms import TagField from dcim.choices import InterfaceModeChoices +from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.forms import INTERFACE_MODE_HELP_TEXT 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, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, +) from ipam.models import IPAddress, VLANGroup, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, - SmallTextarea, StaticSelect2, StaticSelect2Multiple + ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ConfirmationForm, + CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, StaticSelect2, + StaticSelect2Multiple, TagFilterField, ) from .choices import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -73,7 +76,7 @@ class ClusterGroupCSVForm(forms.ModelForm): # Clusters # -class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): comments = CommentField() tags = TagField( required=False @@ -97,7 +100,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class ClusterCSVForm(forms.ModelForm): +class ClusterCSVForm(CustomFieldModelCSVForm): type = forms.ModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', @@ -170,7 +173,8 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ) ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -228,6 +232,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm null_option=True, ) ) + tag = TagFilterField(model) class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): @@ -325,7 +330,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm): # Virtual Machines # -class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): cluster_group = forms.ModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, @@ -428,7 +433,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VirtualMachineCSVForm(forms.ModelForm): +class VirtualMachineCSVForm(CustomFieldModelCSVForm): status = CSVChoiceField( choices=VirtualMachineStatusChoices, required=False, @@ -534,7 +539,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB label='Disk (GB)' ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -634,6 +640,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil required=False, label='MAC address' ) + tag = TagFilterField(model) # @@ -731,7 +738,11 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): self.cleaned_data['tagged_vlans'] = [] -class InterfaceCreateForm(ComponentForm): +class InterfaceCreateForm(BootstrapMixin, forms.Form): + virtual_machine = forms.ModelChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.HiddenInput() + ) name_pattern = ExpandableNameField( label='Name' ) @@ -741,12 +752,13 @@ class InterfaceCreateForm(ComponentForm): widget=forms.HiddenInput() ) enabled = forms.BooleanField( - required=False + required=False, + initial=True ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mac_address = forms.CharField( @@ -785,14 +797,13 @@ class InterfaceCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - - # Set interfaces enabled by default - kwargs['initial'] = kwargs.get('initial', {}).copy() - kwargs['initial'].update({'enabled': True}) - super().__init__(*args, **kwargs) - # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site + virtual_machine = VirtualMachine.objects.get( + pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine') + ) + + # Limit VLAN choices to those in: global vlans, global groups, the current site's group, the current site vlan_choices = [] global_vlans = VLAN.objects.filter(site=None, group=None) vlan_choices.append( @@ -804,7 +815,7 @@ class InterfaceCreateForm(ComponentForm): (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) ) - site = getattr(self.parent.cluster, 'site', None) + site = getattr(virtual_machine.cluster, 'site', None) if site is not None: # Add non-grouped site VLANs @@ -828,14 +839,18 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() ) + virtual_machine = forms.ModelChoiceField( + queryset=VirtualMachine.objects.all(), + widget=forms.HiddenInput() + ) enabled = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect() ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) description = forms.CharField( @@ -874,35 +889,39 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site - vlan_choices = [] - global_vlans = VLAN.objects.filter(site=None, group=None) - vlan_choices.append( - ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) - ) - for group in VLANGroup.objects.filter(site=None): - global_group_vlans = VLAN.objects.filter(group=group) + # Limit available VLANs based on the parent VirtualMachine + if 'virtual_machine' in self.initial: + parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first() + + # Limit VLAN choices to global VLANs, VLANs in global groups, the current site's group, the current site + vlan_choices = [] + global_vlans = VLAN.objects.filter(site=None, group=None) vlan_choices.append( - (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) + ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) ) - if self.parent_obj.cluster is not None: - site = getattr(self.parent_obj.cluster, 'site', None) - if site is not None: + for group in VLANGroup.objects.filter(site=None): + global_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append( + (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) + ) + if parent_obj.cluster is not None: + site = getattr(parent_obj.cluster, 'site', None) + if site is not None: - # Add non-grouped site VLANs - site_vlans = VLAN.objects.filter(site=site, group=None) - vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) + # Add non-grouped site VLANs + site_vlans = VLAN.objects.filter(site=site, group=None) + vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) - # Add grouped site VLANs - for group in VLANGroup.objects.filter(site=site): - site_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append(( - '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in site_group_vlans] - )) + # Add grouped site VLANs + for group in VLANGroup.objects.filter(site=site): + site_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append(( + '{} / {}'.format(group.site.name, group.name), + [(vlan.pk, vlan) for vlan in site_group_vlans] + )) - self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices - self.fields['tagged_vlans'].choices = vlan_choices + self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices + self.fields['tagged_vlans'].choices = vlan_choices # @@ -931,8 +950,8 @@ class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm): ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) description = forms.CharField( diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 57af2ffc8..6cedf9803 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -1,23 +1,23 @@ -import urllib.parse +from netaddr import EUI -from django.test import Client, TestCase -from django.urls import reverse - -from utilities.testing import create_test_user +from dcim.choices import InterfaceModeChoices +from dcim.models import DeviceRole, Interface, Platform, Site +from ipam.models import VLAN +from utilities.testing import StandardTestCases +from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine -class ClusterGroupTestCase(TestCase): +class ClusterGroupTestCase(StandardTestCases.Views): + model = ClusterGroup - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_clustergroup', - 'virtualization.add_clustergroup', - ] - ) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): ClusterGroup.objects.bulk_create([ ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), @@ -25,39 +25,29 @@ class ClusterGroupTestCase(TestCase): ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), ]) - def test_clustergroup_list(self): + cls.form_data = { + 'name': 'Cluster Group X', + 'slug': 'cluster-group-x', + } - url = reverse('virtualization:clustergroup_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_clustergroup_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Cluster Group 4,cluster-group-4", "Cluster Group 5,cluster-group-5", "Cluster Group 6,cluster-group-6", ) - response = self.client.post(reverse('virtualization:clustergroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(ClusterGroup.objects.count(), 6) +class ClusterTypeTestCase(StandardTestCases.Views): + model = ClusterType + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + test_bulk_edit_objects = None -class ClusterTypeTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_clustertype', - 'virtualization.add_clustertype', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): ClusterType.objects.bulk_create([ ClusterType(name='Cluster Type 1', slug='cluster-type-1'), @@ -65,134 +55,229 @@ class ClusterTypeTestCase(TestCase): ClusterType(name='Cluster Type 3', slug='cluster-type-3'), ]) - def test_clustertype_list(self): + cls.form_data = { + 'name': 'Cluster Type X', + 'slug': 'cluster-type-x', + } - url = reverse('virtualization:clustertype_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_clustertype_import(self): - - csv_data = ( + cls.csv_data = ( "name,slug", "Cluster Type 4,cluster-type-4", "Cluster Type 5,cluster-type-5", "Cluster Type 6,cluster-type-6", ) - response = self.client.post(reverse('virtualization:clustertype_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - self.assertEqual(ClusterType.objects.count(), 6) +class ClusterTestCase(StandardTestCases.Views): + model = Cluster + @classmethod + def setUpTestData(cls): -class ClusterTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_cluster', - 'virtualization.add_cluster', - ] + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), ) - self.client = Client() - self.client.force_login(user) + Site.objects.bulk_create(sites) - clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1') - clustergroup.save() + clustergroups = ( + ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), + ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), + ) + ClusterGroup.objects.bulk_create(clustergroups) - clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1') - clustertype.save() + clustertypes = ( + ClusterType(name='Cluster Type 1', slug='cluster-type-1'), + ClusterType(name='Cluster Type 2', slug='cluster-type-2'), + ) + ClusterType.objects.bulk_create(clustertypes) Cluster.objects.bulk_create([ - Cluster(name='Cluster 1', group=clustergroup, type=clustertype), - Cluster(name='Cluster 2', group=clustergroup, type=clustertype), - Cluster(name='Cluster 3', group=clustergroup, type=clustertype), + Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], site=sites[0]), + Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], site=sites[0]), + Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]), ]) - def test_cluster_list(self): - - url = reverse('virtualization:cluster_list') - params = { - "group": ClusterGroup.objects.first().slug, - "type": ClusterType.objects.first().slug, + cls.form_data = { + 'name': 'Cluster X', + 'group': clustergroups[1].pk, + 'type': clustertypes[1].pk, + 'tenant': None, + 'site': sites[1].pk, + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_cluster(self): - - cluster = Cluster.objects.first() - response = self.client.get(cluster.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_cluster_import(self): - - csv_data = ( + cls.csv_data = ( "name,type", "Cluster 4,Cluster Type 1", "Cluster 5,Cluster Type 1", "Cluster 6,Cluster Type 1", ) - response = self.client.post(reverse('virtualization:cluster_import'), {'csv': '\n'.join(csv_data)}) - - self.assertEqual(response.status_code, 200) - self.assertEqual(Cluster.objects.count(), 6) - - -class VirtualMachineTestCase(TestCase): - - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_virtualmachine', - 'virtualization.add_virtualmachine', - ] - ) - self.client = Client() - self.client.force_login(user) - - clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1') - clustertype.save() - - cluster = Cluster(name='Cluster 1', type=clustertype) - cluster.save() - - VirtualMachine.objects.bulk_create([ - VirtualMachine(name='Virtual Machine 1', cluster=cluster), - VirtualMachine(name='Virtual Machine 2', cluster=cluster), - VirtualMachine(name='Virtual Machine 3', cluster=cluster), - ]) - - def test_virtualmachine_list(self): - - url = reverse('virtualization:virtualmachine_list') - params = { - "cluster_id": Cluster.objects.first().pk, + cls.bulk_edit_data = { + 'group': clustergroups[1].pk, + 'type': clustertypes[1].pk, + 'tenant': None, + 'site': sites[1].pk, + 'comments': 'New comments', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - def test_virtualmachine(self): +class VirtualMachineTestCase(StandardTestCases.Views): + model = VirtualMachine - virtualmachine = VirtualMachine.objects.first() - response = self.client.get(virtualmachine.get_absolute_url()) - self.assertEqual(response.status_code, 200) + @classmethod + def setUpTestData(cls): - def test_virtualmachine_import(self): + deviceroles = ( + DeviceRole(name='Device Role 1', slug='device-role-1'), + DeviceRole(name='Device Role 2', slug='device-role-2'), + ) + DeviceRole.objects.bulk_create(deviceroles) - csv_data = ( + platforms = ( + Platform(name='Platform 1', slug='platform-1'), + Platform(name='Platform 2', slug='platform-2'), + ) + Platform.objects.bulk_create(platforms) + + clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + + clusters = ( + Cluster(name='Cluster 1', type=clustertype), + Cluster(name='Cluster 2', type=clustertype), + ) + Cluster.objects.bulk_create(clusters) + + VirtualMachine.objects.bulk_create([ + VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]), + VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]), + VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]), + ]) + + cls.form_data = { + 'cluster': clusters[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'name': 'Virtual Machine X', + 'status': VirtualMachineStatusChoices.STATUS_STAGED, + 'role': deviceroles[1].pk, + 'primary_ip4': None, + 'primary_ip6': None, + 'vcpus': 4, + 'memory': 32768, + 'disk': 4000, + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', + 'local_context_data': None, + } + + cls.csv_data = ( "name,cluster", "Virtual Machine 4,Cluster 1", "Virtual Machine 5,Cluster 1", "Virtual Machine 6,Cluster 1", ) - response = self.client.post(reverse('virtualization:virtualmachine_import'), {'csv': '\n'.join(csv_data)}) + cls.bulk_edit_data = { + 'cluster': clusters[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'status': VirtualMachineStatusChoices.STATUS_STAGED, + 'role': deviceroles[1].pk, + 'vcpus': 8, + 'memory': 65535, + 'disk': 8000, + 'comments': 'New comments', + } - self.assertEqual(response.status_code, 200) - self.assertEqual(VirtualMachine.objects.count(), 6) + +class InterfaceTestCase(StandardTestCases.Views): + model = Interface + + # Disable inapplicable tests + test_list_objects = None + test_create_object = None + test_import_objects = None + + def test_bulk_create_objects(self): + return self._test_bulk_create_objects(expected_count=3) + + def _get_base_url(self): + # Interface belongs to the DCIM app, so we have to override the base URL + return 'virtualization:interface_{}' + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site) + virtualmachines = ( + VirtualMachine(name='Virtual Machine 1', cluster=cluster, role=devicerole), + VirtualMachine(name='Virtual Machine 2', cluster=cluster, role=devicerole), + ) + VirtualMachine.objects.bulk_create(virtualmachines) + + Interface.objects.bulk_create([ + Interface(virtual_machine=virtualmachines[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(virtual_machine=virtualmachines[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(virtual_machine=virtualmachines[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL), + ]) + + vlans = ( + VLAN(vid=1, name='VLAN1', site=site), + VLAN(vid=101, name='VLAN101', site=site), + VLAN(vid=102, name='VLAN102', site=site), + VLAN(vid=103, name='VLAN103', site=site), + ) + VLAN.objects.bulk_create(vlans) + + cls.form_data = { + 'virtual_machine': virtualmachines[1].pk, + 'name': 'Interface X', + 'type': InterfaceTypeChoices.TYPE_VIRTUAL, + 'enabled': False, + 'mgmt_only': False, + 'mac_address': EUI('01-02-03-04-05-06'), + 'mtu': 2000, + 'description': 'New description', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'virtual_machine': virtualmachines[1].pk, + 'name_pattern': 'Interface [4-6]', + 'type': InterfaceTypeChoices.TYPE_VIRTUAL, + 'enabled': False, + 'mgmt_only': False, + 'mac_address': EUI('01-02-03-04-05-06'), + 'mtu': 2000, + 'description': 'New description', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'virtual_machine': virtualmachines[1].pk, + 'enabled': False, + 'mtu': 2000, + 'description': 'New description', + 'mode': InterfaceModeChoices.MODE_TAGGED, + # 'untagged_vlan': vlans[0].pk, + # 'tagged_vlans': [v.pk for v in vlans[1:4]], + } + + cls.csv_data = ( + "device,name,type", + "Device 1,Interface 4,1000BASE-T (1GE)", + "Device 1,Interface 5,1000BASE-T (1GE)", + "Device 1,Interface 6,1000BASE-T (1GE)", + ) diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index 7cc28be51..557f8a9ca 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -9,53 +9,53 @@ app_name = 'virtualization' urlpatterns = [ # Cluster types - path(r'cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'), - path(r'cluster-types/add/', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), - path(r'cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), - path(r'cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), - path(r'cluster-types//edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), - path(r'cluster-types//changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), + path('cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'), + path('cluster-types/add/', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), + path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), + path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), + path('cluster-types//edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), + path('cluster-types//changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), # Cluster groups - path(r'cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), - path(r'cluster-groups/add/', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), - path(r'cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), - path(r'cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), - path(r'cluster-groups//edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), - path(r'cluster-groups//changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), + path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), + path('cluster-groups/add/', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), + path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), + path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), + path('cluster-groups//edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), + path('cluster-groups//changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), # Clusters - path(r'clusters/', views.ClusterListView.as_view(), name='cluster_list'), - path(r'clusters/add/', views.ClusterCreateView.as_view(), name='cluster_add'), - path(r'clusters/import/', views.ClusterBulkImportView.as_view(), name='cluster_import'), - path(r'clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), - path(r'clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), - path(r'clusters//', views.ClusterView.as_view(), name='cluster'), - path(r'clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), - path(r'clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), - path(r'clusters//changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), - path(r'clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), - path(r'clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), + path('clusters/', views.ClusterListView.as_view(), name='cluster_list'), + path('clusters/add/', views.ClusterCreateView.as_view(), name='cluster_add'), + path('clusters/import/', views.ClusterBulkImportView.as_view(), name='cluster_import'), + path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), + path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), + path('clusters//', views.ClusterView.as_view(), name='cluster'), + path('clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), + path('clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), + path('clusters//changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), + path('clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), + path('clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), # Virtual machines - path(r'virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), - path(r'virtual-machines/add/', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), - path(r'virtual-machines/import/', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), - path(r'virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), - path(r'virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), - path(r'virtual-machines//', views.VirtualMachineView.as_view(), name='virtualmachine'), - path(r'virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), - path(r'virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), - path(r'virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), - path(r'virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), - path(r'virtual-machines//services/assign/', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), + path('virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), + path('virtual-machines/add/', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), + path('virtual-machines/import/', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), + path('virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), + path('virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), + path('virtual-machines//', views.VirtualMachineView.as_view(), name='virtualmachine'), + path('virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), + path('virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), + path('virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), + path('virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), + path('virtual-machines//services/assign/', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), # VM interfaces - path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), - path(r'virtual-machines//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), - path(r'virtual-machines//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - path(r'virtual-machines//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - path(r'vm-interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), - path(r'vm-interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), ] diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 646fb000d..b961d65e5 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -330,8 +330,6 @@ class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interface' - parent_model = VirtualMachine - parent_field = 'virtual_machine' model = Interface form = forms.InterfaceCreateForm model_form = forms.InterfaceForm @@ -353,7 +351,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' queryset = Interface.objects.all() - parent_model = VirtualMachine table = tables.InterfaceTable form = forms.InterfaceBulkEditForm @@ -361,7 +358,6 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interface' queryset = Interface.objects.all() - parent_model = VirtualMachine table = tables.InterfaceTable