diff --git a/docs/api/filtering.md b/docs/api/filtering.md index e7b51d303..6bc47e75b 100644 --- a/docs/api/filtering.md +++ b/docs/api/filtering.md @@ -17,7 +17,7 @@ E.g. filtering based on a device's name: While you are able to filter based on an arbitrary number of fields, you are also able to pass multiple values for the same field. In most cases filtering on multiple values is -implemented as a logical OR operation. A notible exception is the `tag` filter which +implemented as a logical OR operation. A notable exception is the `tag` filter which is a logical AND. Passing multiple values for one field, can be combined with other fields. For example, filtering for devices with either the name of DC-SPINE-1 _or_ DC-LEAF-4: @@ -33,11 +33,11 @@ _both_ of those tags applied: ## Lookup Expressions -Certain model fields also support filtering using additonal lookup expressions. This allows +Certain model fields also support filtering using additional lookup expressions. This allows for negation and other context specific filtering. These lookup expressions can be applied by adding a suffix to the desired field's name. -E.g. `mac_address__n`. In this case, the filter expression is for negation and it is seperated +E.g. `mac_address__n`. In this case, the filter expression is for negation and it is separated by two underscores. Below are the lookup expressions that are supported across different field types. diff --git a/docs/api/overview.md b/docs/api/overview.md index 81e4caa25..8eefae027 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -243,16 +243,17 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_ ## Filtering -A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`): +A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (identified by the slug `active`): ``` -GET /api/ipam/prefixes/?status=1 +GET /api/ipam/prefixes/?status=active ``` The choices available for fixed choice fields such as `status` can be retrieved by sending an `OPTIONS` API request for the desired endpoint: ```no-highlight $ curl -s -X OPTIONS \ +-H "Authorization: Token $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json; indent=4" \ http://localhost:8000/api/ipam/prefixes/ | jq ".actions.POST.status.choices" @@ -274,7 +275,6 @@ http://localhost:8000/api/ipam/prefixes/ | jq ".actions.POST.status.choices" "display_name": "Deprecated" } ] - ``` For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar". diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 503ed1954..3f2b29b87 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -165,6 +165,21 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni --- +## HTTP_PROXIES + +Default: None + +A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhooks). Proxies should be specified by schema as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example: + +```python +HTTP_PROXIES = { + 'http': 'http://10.10.1.10:3128', + 'https': 'http://10.10.1.10:1080', +} +``` + +--- + ## LOGGING By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`. diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index f5244bff5..d924d2c0b 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -35,13 +35,9 @@ Update the following static libraries to their most recent stable release: * jQuery * jQuery UI -### Squash Schema Migrations - -Database schema migrations should be squashed for each new minor release. See the [squashing guide](squashing-migrations.md) for the detailed process. - ### Create a new Release Notes Page -Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for the new release. Add the file to the table of contents within `mkdocs.yml`. +Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for the new release. Add the file to the table of contents within `mkdocs.yml`, and point `index.md` to the new file. ### Manually Perform a New Install diff --git a/docs/development/squashing-migrations.md b/docs/development/squashing-migrations.md deleted file mode 100644 index bc0c0548f..000000000 --- a/docs/development/squashing-migrations.md +++ /dev/null @@ -1,168 +0,0 @@ -# Squashing Database Schema Migrations - -## What are Squashed Migrations? - -The Django framework on which NetBox is built utilizes [migration files](https://docs.djangoproject.com/en/stable/topics/migrations/) to keep track of changes to the PostgreSQL database schema. Each time a model is altered, the resulting schema change is captured in a migration file, which can then be applied to effect the new schema. - -As changes are made over time, more and more migration files are created. Although not necessarily problematic, it can be beneficial to merge and compress these files occasionally to reduce the total number of migrations that need to be applied upon installation of NetBox. This merging process is called _squashing_ in Django vernacular, and results in two parallel migration paths: individual and squashed. - -Below is an example showing both individual and squashed migration files within an app: - -| Individual | Squashed | -|------------|----------| -| 0001_initial | 0001_initial_squashed_0004_add_field | -| 0002_alter_field | . | -| 0003_remove_field | . | -| 0004_add_field | . | -| 0005_another_field | 0005_another_field | - -In the example above, a new installation can leverage the squashed migrations to apply only two migrations: - -* `0001_initial_squashed_0004_add_field` -* `0005_another_field` - -This is because the squash file contains all of the operations performed by files `0001` through `0004`. - -However, an existing installation that has already applied some of the individual migrations contained within the squash file must continue applying individual migrations. For instance, an installation which currently has up to `0002_alter_field` applied must apply the following migrations to become current: - -* `0003_remove_field` -* `0004_add_field` -* `0005_another_field` - -Squashed migrations are opportunistic: They are used only if applicable to the current environment. Django will fall back to using individual migrations if the squashed migrations do not agree with the current database schema at any point. - -## Squashing Migrations - -During every minor (i.e. 2.x) release, migrations should be squashed to help simplify the migration process for new installations. The process below describes how to squash migrations efficiently and with minimal room for error. - -### 1. Create a New Branch - -Create a new branch off of the `develop-2.x` branch. (Migrations should be squashed _only_ in preparation for a new minor release.) - -``` -git checkout -B squash-migrations -``` - -### 2. Delete Existing Squash Files - -Delete the most recent squash file within each NetBox app. This allows us to extend squash files where the opportunity exists. For example, we might be able to replace `0005_to_0008` with `0005_to_0011`. - -### 3. Generate the Current Migration Plan - -Use Django's `showmigrations` utility to display the order in which all migrations would be applied for a new installation. - -``` -manage.py showmigrations --plan -``` - -From the resulting output, delete all lines which reference an external migration. Any migrations imposed by Django itself on an external package are not relevant. - -### 4. Create Squash Files - -Begin iterating through the migration plan, looking for successive sets of migrations within an app. These are candidates for squashing. For example: - -``` -[X] extras.0014_configcontexts -[X] extras.0015_remove_useraction -[X] extras.0016_exporttemplate_add_cable -[X] extras.0017_exporttemplate_mime_type_length -[ ] extras.0018_exporttemplate_add_jinja2 -[ ] extras.0019_tag_taggeditem -[X] dcim.0062_interface_mtu -[X] dcim.0063_device_local_context_data -[X] dcim.0064_remove_platform_rpc_client -[ ] dcim.0065_front_rear_ports -[X] circuits.0001_initial_squashed_0010_circuit_status -[ ] dcim.0066_cables -... -``` - -Migrations `0014` through `0019` in `extras` can be squashed, as can migrations `0062` through `0065` in `dcim`. Migration `0066` cannot be included in the same squash file, because the `circuits` migration must be applied before it. (Note that whether or not each migration is currently applied to the database does not matter.) - -Squash files are created using Django's `squashmigrations` utility: - -``` -manage.py squashmigrations -``` - -For example, our first step in the example would be to run `manage.py squashmigrations extras 0014 0019`. - -!!! note - Specifying a migration file's numeric index is enough to uniquely identify it within an app. There is no need to specify the full filename. - -This will create a new squash file within the app's `migrations` directory, named as a concatenation of its beginning and ending migration. Some manual editing is necessary for each new squash file for housekeeping purposes: - -* Remove the "automatically generated" comment at top (to indicate that a human has reviewed the file). -* Reorder `import` statements as necessary per PEP8. -* It may be necessary to copy over custom functions from the original migration files (this will be indicated by a comment near the top of the squash file). It is safe to remove any functions that exist solely to accomodate reverse migrations (which we no longer support). - -Repeat this process for each candidate set of migrations until you reach the end of the migration plan. - -### 5. Check for Missing Migrations - -If everything went well, at this point we should have a completed squashed path. Perform a dry run to check for any missing migrations: - -``` -manage.py migrate --dry-run -``` - -### 5. Run Migrations - -Next, we'll apply the entire migration path to an empty database. Begin by dropping and creating your development database. - -!!! warning - Obviously, first back up any data you don't want to lose. - -``` -sudo -u postgres psql -c 'drop database netbox' -sudo -u postgres psql -c 'create database netbox' -``` - -Apply the migrations with the `migrate` management command. It is not necessary to specify a particular migration path; Django will detect and use the squashed migrations automatically. You can verify the exact migrations being applied by enabling verboes output with `-v 2`. - -``` -manage.py migrate -v 2 -``` - -### 6. Commit the New Migrations - -If everything is successful to this point, commit your changes to the `squash-migrations` branch. - -### 7. Validate Resulting Schema - -To ensure our new squashed migrations do not result in a deviation from the original schema, we'll compare the two. With the new migration file safely commit, check out the `develop-2.x` branch, which still contains only the individual migrations. - -``` -git checkout develop-2.x -``` - -Temporarily install the [django-extensions](https://django-extensions.readthedocs.io/) package, which provides the `sqldiff utility`: - -``` -pip install django-extensions -``` - -Also add `django_extensions` to `INSTALLED_APPS` in `netbox/netbox/settings.py`. - -At this point, our database schema has been defined by using the squashed migrations. We can run `sqldiff` to see if it differs any from what the current (non-squashed) migrations would generate. `sqldiff` accepts a list of apps against which to run: - -``` -manage.py sqldiff circuits dcim extras ipam secrets tenancy users virtualization -``` - -It is safe to ignore errors indicating an "unknown database type" for the following fields: - -* `dcim_interface.mac_address` -* `ipam_aggregate.prefix` -* `ipam_prefix.prefix` - -It is also safe to ignore the message "Table missing: extras_script". - -Resolve any differences by correcting migration files in the `squash-migrations` branch. - -!!! warning - Don't forget to remove `django_extension` from `INSTALLED_APPS` before committing your changes. - -### 8. Merge the Squashed Migrations - -Once all squashed migrations have been validated and all tests run successfully, merge the `squash-migrations` branch into `develop-2.x`. This completes the squashing process. diff --git a/docs/development/user-preferences.md b/docs/development/user-preferences.md new file mode 100644 index 000000000..80088186c --- /dev/null +++ b/docs/development/user-preferences.md @@ -0,0 +1,11 @@ +# User Preferences + +The `users.UserConfig` model holds individual preferences for each user in the form of JSON data. This page serves as a manifest of all recognized user preferences in NetBox. + +## Available Preferences + +| Name | Description | +| ---- | ----------- | +| extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) | +| pagination.per_page | The number of items to display per page of a paginated table | +| tables.${table_name}.columns | The ordered list of columns to display when viewing the table | diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 414c3c907..afe3a51d2 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -20,10 +20,10 @@ If a recent enough version of PostgreSQL is not available through your distribut #### CentOS -CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6. +CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6, however you may opt to install a more recent version. ```no-highlight -# yum install -y https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm +# yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm # yum install -y postgresql96 postgresql96-server postgresql96-devel # /usr/pgsql-9.6/bin/postgresql96-setup initdb ``` diff --git a/docs/installation/5-ldap.md b/docs/installation/5-ldap.md index b263ae040..2fd88b841 100644 --- a/docs/installation/5-ldap.md +++ b/docs/installation/5-ldap.md @@ -135,7 +135,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600 ## Troubleshooting LDAP -`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`. +`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`. For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`. diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index e44a306fe..364b2cd9d 120000 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -1 +1 @@ -version-2.7.md \ No newline at end of file +version-2.8.md \ No newline at end of file diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index e6eabf8ca..e75bf4ab9 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -1,5 +1,31 @@ # NetBox v2.8 +## v2.8.2 (2020-05-06) + +### Enhancements + +* [#492](https://github.com/netbox-community/netbox/issues/492) - Enable toggling and rearranging table columns +* [#3147](https://github.com/netbox-community/netbox/issues/3147) - Allow specifying related objects by arbitrary attribute during CSV import +* [#3064](https://github.com/netbox-community/netbox/issues/3064) - Include tags in object lists as a toggleable table column +* [#3294](https://github.com/netbox-community/netbox/issues/3294) - Implement mechanism for storing user preferences +* [#4421](https://github.com/netbox-community/netbox/issues/4421) - Retain user's preference for config context format +* [#4502](https://github.com/netbox-community/netbox/issues/4502) - Enable configuration of proxies for outbound HTTP requests +* [#4531](https://github.com/netbox-community/netbox/issues/4531) - Retain user's preference for page length +* [#4554](https://github.com/netbox-community/netbox/issues/4554) - Add ServerTech's HDOT Cx power outlet type + +### Bug Fixes + +* [#4527](https://github.com/netbox-community/netbox/issues/4527) - Fix assignment of certain tags to config contexts +* [#4545](https://github.com/netbox-community/netbox/issues/4545) - Removed all squashed schema migrations to allow direct upgrades from very old releases +* [#4548](https://github.com/netbox-community/netbox/issues/4548) - Fix tracing cables through a single RearPort +* [#4549](https://github.com/netbox-community/netbox/issues/4549) - Fix encoding unicode webhook body data +* [#4556](https://github.com/netbox-community/netbox/issues/4556) - Update form for adding devices to clusters +* [#4578](https://github.com/netbox-community/netbox/issues/4578) - Prevent setting 0U height on device type with racked instances +* [#4584](https://github.com/netbox-community/netbox/issues/4584) - Ensure consistent support for filtering objects by `id` across all REST API endpoints +* [#4588](https://github.com/netbox-community/netbox/issues/4588) - Restore ability to add/remove tags on services, virtual chassis in bulk + +--- + ## v2.8.1 (2020-04-23) ### Notes diff --git a/mkdocs.yml b/mkdocs.yml index d1ced6d8c..b8633ea8f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,8 +72,8 @@ nav: - Utility Views: 'development/utility-views.md' - Extending Models: 'development/extending-models.md' - Application Registry: 'development/application-registry.md' + - User Preferences: 'development/user-preferences.md' - Release Checklist: 'development/release-checklist.md' - - Squashing Migrations: 'development/squashing-migrations.md' - Release Notes: - Version 2.8: 'release-notes/version-2.8.md' - Version 2.7: 'release-notes/version-2.7.md' diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index b8d97d77d..206dcc305 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -51,7 +51,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilte class Meta: model = Provider - fields = ['name', 'slug', 'asn', 'account'] + fields = ['id', 'name', 'slug', 'asn', 'account'] def search(self, queryset, name, value): if not value.strip(): @@ -129,7 +129,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr class Meta: model = Circuit - fields = ['cid', 'install_date', 'commit_rate'] + fields = ['id', 'cid', 'install_date', 'commit_rate'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index c6f0dfdc4..427dc2e89 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -8,9 +8,9 @@ from extras.forms import ( from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, - StaticSelect2Multiple, TagFilterField, + APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, + CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, + StaticSelect2, StaticSelect2Multiple, TagFilterField, ) from .choices import CircuitStatusChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -55,12 +55,6 @@ class ProviderCSVForm(CustomFieldModelCSVForm): class Meta: model = Provider fields = Provider.csv_headers - help_texts = { - 'name': 'Provider name', - 'asn': '32-bit autonomous system number', - 'portal_url': 'Portal URL', - 'comments': 'Free-form comments', - } class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -148,7 +142,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): ] -class CircuitTypeCSVForm(forms.ModelForm): +class CircuitTypeCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -192,35 +186,26 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class CircuitCSVForm(CustomFieldModelCSVForm): - provider = forms.ModelChoiceField( + provider = CSVModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', - help_text='Name of parent provider', - error_messages={ - 'invalid_choice': 'Provider not found.' - } + help_text='Assigned provider' ) - type = forms.ModelChoiceField( + type = CSVModelChoiceField( queryset=CircuitType.objects.all(), to_field_name='name', - help_text='Type of circuit', - error_messages={ - 'invalid_choice': 'Invalid circuit type.' - } + help_text='Type of circuit' ) status = CSVChoiceField( choices=CircuitStatusChoices, required=False, help_text='Operational status' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.' - } + help_text='Assigned tenant' ) class Meta: diff --git a/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py b/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py deleted file mode 100644 index 4eec30667..000000000 --- a/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py +++ /dev/null @@ -1,134 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - -import dcim.fields - - -def circuits_to_terms(apps, schema_editor): - Circuit = apps.get_model('circuits', 'Circuit') - CircuitTermination = apps.get_model('circuits', 'CircuitTermination') - for c in Circuit.objects.all(): - CircuitTermination( - circuit=c, - term_side=b'A', - site=c.site, - interface=c.interface, - port_speed=c.port_speed, - upstream_speed=c.upstream_speed, - xconnect_id=c.xconnect_id, - pp_info=c.pp_info, - ).save() - - -class Migration(migrations.Migration): - - replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations')] - - dependencies = [ - ('tenancy', '0001_initial'), - ('dcim', '0001_initial'), - ('dcim', '0022_color_names_to_rgb'), - ] - - operations = [ - migrations.CreateModel( - name='CircuitType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Provider', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN')), - ('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')), - ('portal_url', models.URLField(blank=True, verbose_name=b'Portal')), - ('noc_contact', models.TextField(blank=True, verbose_name=b'NOC contact')), - ('admin_contact', models.TextField(blank=True, verbose_name=b'Admin contact')), - ('comments', models.TextField(blank=True)), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Circuit', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')), - ('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')), - ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), - ('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')), - ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), - ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), - ('comments', models.TextField(blank=True)), - ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')), - ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')), - ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), - ], - options={ - 'ordering': ['provider', 'cid'], - 'unique_together': {('provider', 'cid')}, - }, - ), - migrations.CreateModel( - name='CircuitTermination', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1, verbose_name='Termination')), - ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), - ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), - ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), - ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), - ('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')), - ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit_termination', to='dcim.Interface')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')), - ], - options={ - 'ordering': ['circuit', 'term_side'], - 'unique_together': {('circuit', 'term_side')}, - }, - ), - migrations.RunPython( - code=circuits_to_terms, - ), - migrations.RemoveField( - model_name='circuit', - name='interface', - ), - migrations.RemoveField( - model_name='circuit', - name='port_speed', - ), - migrations.RemoveField( - model_name='circuit', - name='pp_info', - ), - migrations.RemoveField( - model_name='circuit', - name='site', - ), - migrations.RemoveField( - model_name='circuit', - name='upstream_speed', - ), - migrations.RemoveField( - model_name='circuit', - name='xconnect_id', - ), - ] diff --git a/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py b/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py deleted file mode 100644 index 5bcd863a4..000000000 --- a/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py +++ /dev/null @@ -1,254 +0,0 @@ -import sys - -import django.db.models.deletion -import taggit.managers -from django.db import migrations, models - -import dcim.fields - -CONNECTION_STATUS_CONNECTED = True - -CIRCUIT_STATUS_CHOICES = ( - (0, 'deprovisioning'), - (1, 'active'), - (2, 'planned'), - (3, 'provisioning'), - (4, 'offline'), - (5, 'decommissioned') -) - - -def circuit_terminations_to_cables(apps, schema_editor): - """ - Copy all existing CircuitTermination Interface associations as Cables - """ - ContentType = apps.get_model('contenttypes', 'ContentType') - CircuitTermination = apps.get_model('circuits', 'CircuitTermination') - Interface = apps.get_model('dcim', 'Interface') - Cable = apps.get_model('dcim', 'Cable') - - # Load content types - circuittermination_type = ContentType.objects.get_for_model(CircuitTermination) - interface_type = ContentType.objects.get_for_model(Interface) - - # Create a new Cable instance from each console connection - if 'test' not in sys.argv: - print("\n Adding circuit terminations... ", end='', flush=True) - for circuittermination in CircuitTermination.objects.filter(interface__isnull=False): - - # Create the new Cable - cable = Cable.objects.create( - termination_a_type=circuittermination_type, - termination_a_id=circuittermination.id, - termination_b_type=interface_type, - termination_b_id=circuittermination.interface_id, - status=CONNECTION_STATUS_CONNECTED - ) - - # Cache the Cable on its two termination points - CircuitTermination.objects.filter(pk=circuittermination.pk).update( - cable=cable, - connected_endpoint=circuittermination.interface, - connection_status=CONNECTION_STATUS_CONNECTED - ) - # Cache the connected Cable on the Interface - Interface.objects.filter(pk=circuittermination.interface_id).update( - cable=cable, - _connected_circuittermination=circuittermination, - connection_status=CONNECTION_STATUS_CONNECTED - ) - - cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count() - if 'test' not in sys.argv: - print("{} cables created".format(cable_count)) - - -def circuit_status_to_slug(apps, schema_editor): - Circuit = apps.get_model('circuits', 'Circuit') - for id, slug in CIRCUIT_STATUS_CHOICES: - Circuit.objects.filter(status=str(id)).update(status=slug) - - -class Migration(migrations.Migration): - - replaces = [('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status'), ('circuits', '0011_tags'), ('circuits', '0012_change_logging'), ('circuits', '0013_cables'), ('circuits', '0014_circuittermination_description'), ('circuits', '0015_custom_tag_models'), ('circuits', '0016_3569_circuit_fields'), ('circuits', '0017_circuittype_description')] - - dependencies = [ - ('circuits', '0006_terminations'), - ('extras', '0019_tag_taggeditem'), - ('taggit', '0002_auto_20150616_2121'), - ('dcim', '0066_cables'), - ] - - operations = [ - migrations.AddField( - model_name='circuit', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AlterField( - model_name='circuittermination', - name='interface', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'), - ), - migrations.AlterField( - model_name='circuit', - name='cid', - field=models.CharField(max_length=50, verbose_name='Circuit ID'), - ), - migrations.AlterField( - model_name='circuit', - name='commit_rate', - field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'), - ), - migrations.AlterField( - model_name='circuit', - name='install_date', - field=models.DateField(blank=True, null=True, verbose_name='Date installed'), - ), - migrations.AlterField( - model_name='circuittermination', - name='port_speed', - field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'), - ), - migrations.AlterField( - model_name='circuittermination', - name='pp_info', - field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'), - ), - migrations.AlterField( - model_name='circuittermination', - name='term_side', - field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'), - ), - migrations.AlterField( - model_name='circuittermination', - name='upstream_speed', - field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'), - ), - migrations.AlterField( - model_name='circuittermination', - name='xconnect_id', - field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'), - ), - migrations.AlterField( - model_name='provider', - name='account', - field=models.CharField(blank=True, max_length=30, verbose_name='Account number'), - ), - migrations.AlterField( - model_name='provider', - name='admin_contact', - field=models.TextField(blank=True, verbose_name='Admin contact'), - ), - migrations.AlterField( - model_name='provider', - name='asn', - field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), - ), - migrations.AlterField( - model_name='provider', - name='noc_contact', - field=models.TextField(blank=True, verbose_name='NOC contact'), - ), - migrations.AlterField( - model_name='provider', - name='portal_url', - field=models.URLField(blank=True, verbose_name='Portal'), - ), - migrations.AddField( - model_name='circuit', - name='status', - field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1), - ), - migrations.AddField( - model_name='circuit', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='provider', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='circuittype', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='circuittype', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='circuit', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='circuit', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='provider', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='provider', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='circuittermination', - name='connected_endpoint', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'), - ), - migrations.AddField( - model_name='circuittermination', - name='connection_status', - field=models.NullBooleanField(), - ), - migrations.AddField( - model_name='circuittermination', - name='cable', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'), - ), - migrations.RunPython( - code=circuit_terminations_to_cables, - ), - migrations.RemoveField( - model_name='circuittermination', - name='interface', - ), - migrations.AddField( - model_name='circuittermination', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AlterField( - model_name='circuit', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='provider', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='circuit', - name='status', - field=models.CharField(default='active', max_length=50), - ), - migrations.RunPython( - code=circuit_status_to_slug, - ), - migrations.AddField( - model_name='circuittype', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - ] diff --git a/netbox/circuits/migrations/0008_standardize_description.py b/netbox/circuits/migrations/0018_standardize_description.py similarity index 88% rename from netbox/circuits/migrations/0008_standardize_description.py rename to netbox/circuits/migrations/0018_standardize_description.py index fecdee3ca..a0a213e17 100644 --- a/netbox/circuits/migrations/0008_standardize_description.py +++ b/netbox/circuits/migrations/0018_standardize_description.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('circuits', '0007_circuit_add_description_squashed_0017_circuittype_description'), + ('circuits', '0017_circuittype_description'), ] operations = [ diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index e9e8f8aa1..57d41a994 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -38,7 +38,8 @@ class Provider(ChangeLoggedModel, CustomFieldModel): asn = ASNField( blank=True, null=True, - verbose_name='ASN' + verbose_name='ASN', + help_text='32-bit autonomous system number' ) account = models.CharField( max_length=30, @@ -47,7 +48,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel): ) portal_url = models.URLField( blank=True, - verbose_name='Portal' + verbose_name='Portal URL' ) noc_contact = models.TextField( blank=True, diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index a425b3ace..ea17031a1 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from tenancy.tables import COL_TENANT -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, TagColumn, ToggleColumn from .models import Circuit, CircuitType, Provider CIRCUITTYPE_ACTIONS = """ @@ -27,18 +27,20 @@ STATUS_LABEL = """ class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + circuit_count = tables.Column( + accessor=Accessor('count_circuits'), + verbose_name='Circuits' + ) + tags = TagColumn( + url_name='circuits:provider_list' + ) class Meta(BaseTable.Meta): model = Provider - fields = ('pk', 'name', 'asn', 'account',) - - -class ProviderDetailTable(ProviderTable): - circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits') - - class Meta(ProviderTable.Meta): - model = Provider - fields = ('pk', 'name', 'asn', 'account', 'circuit_count') + fields = ( + 'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags', + ) + default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') # @@ -48,7 +50,9 @@ class ProviderDetailTable(ProviderTable): class CircuitTypeTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - circuit_count = tables.Column(verbose_name='Circuits') + circuit_count = tables.Column( + verbose_name='Circuits' + ) actions = tables.TemplateColumn( template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -58,6 +62,7 @@ class CircuitTypeTable(BaseTable): class Meta(BaseTable.Meta): model = CircuitType fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') # @@ -66,17 +71,33 @@ class CircuitTypeTable(BaseTable): class CircuitTable(BaseTable): pk = ToggleColumn() - cid = tables.LinkColumn(verbose_name='ID') - provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + cid = tables.LinkColumn( + verbose_name='ID' + ) + provider = tables.LinkColumn( + viewname='circuits:provider', + args=[Accessor('provider.slug')] + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) a_side = tables.Column( verbose_name='A Side' ) z_side = tables.Column( verbose_name='Z Side' ) + tags = TagColumn( + url_name='circuits:circuit_list' + ) class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') + fields = ( + 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', + 'description', 'tags', + ) + default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description') diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py index 346d2ad80..9756c320b 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -54,6 +54,10 @@ class ProviderTestCase(TestCase): CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000), )) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Provider 1', 'Provider 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -178,6 +182,10 @@ class CircuitTestCase(TestCase): )) CircuitTermination.objects.bulk_create(circuit_terminations) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cid(self): params = {'cid': ['Test Circuit 1', 'Test Circuit 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index b092e1855..709d2a726 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -28,7 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView): queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet filterset_form = forms.ProviderFilterForm - table = tables.ProviderDetailTable + table = tables.ProviderTable class ProviderView(PermissionRequiredMixin, View): @@ -87,7 +87,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_provider' - queryset = Provider.objects.all() + queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm @@ -96,7 +96,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_provider' - queryset = Provider.objects.all() + queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet table = tables.ProviderTable default_return_url = 'circuits:provider_list' diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 32802c61f..8433bb152 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -424,6 +424,8 @@ class PowerOutletTypeChoices(ChoiceSet): TYPE_ITA_M = 'ita-m' TYPE_ITA_N = 'ita-n' TYPE_ITA_O = 'ita-o' + # Proprietary + TYPE_HDOT_CX = 'hdot-cx' CHOICES = ( ('IEC 60320', ( @@ -487,6 +489,9 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_ITA_N, 'ITA Type N'), (TYPE_ITA_O, 'ITA Type O'), )), + ('Proprietary', ( + (TYPE_HDOT_CX, 'HDOT Cx'), + )), ) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1fa7e7210..5bc6dd7f0 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -301,7 +301,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet): class Meta: model = RackReservation - fields = ['created'] + fields = ['id', 'created'] def search(self, queryset, name, value): if not value.strip(): @@ -369,7 +369,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil class Meta: model = DeviceType fields = [ - 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', ] def search(self, queryset, name, value): @@ -1268,7 +1268,7 @@ class PowerPanelFilterSet(BaseFilterSet): class Meta: model = PowerPanel - fields = ['name'] + fields = ['id', 'name'] def search(self, queryset, name, value): if not value.strip(): @@ -1321,7 +1321,7 @@ class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt class Meta: model = PowerFeed - fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'] + fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 29710971e..b104124b4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -5,6 +5,7 @@ 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.utils.safestring import mark_safe from mptt.forms import TreeNodeChoiceField from netaddr import EUI from netaddr.core import AddrFormatError @@ -22,9 +23,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, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, form_from_model, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, + CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, + JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -192,24 +193,17 @@ class RegionForm(BootstrapMixin, forms.ModelForm): ) -class RegionCSVForm(forms.ModelForm): - parent = forms.ModelChoiceField( +class RegionCSVForm(CSVModelForm): + parent = CSVModelChoiceField( queryset=Region.objects.all(), required=False, to_field_name='name', - help_text='Name of parent region', - error_messages={ - 'invalid_choice': 'Region not found.', - } + help_text='Name of parent region' ) class Meta: model = Region fields = Region.csv_headers - help_texts = { - 'name': 'Region name', - 'slug': 'URL-friendly slug', - } class RegionFilterForm(BootstrapMixin, forms.Form): @@ -276,32 +270,26 @@ class SiteCSVForm(CustomFieldModelCSVForm): required=False, help_text='Operational status' ) - region = forms.ModelChoiceField( + region = CSVModelChoiceField( queryset=Region.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned region', - error_messages={ - 'invalid_choice': 'Region not found.', - } + help_text='Assigned region' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: model = Site fields = Site.csv_headers help_texts = { - 'name': 'Site name', - 'slug': 'URL-friendly slug', - 'asn': '32-bit autonomous system number', + 'time_zone': mark_safe( + 'Time zone (available options)' + ) } @@ -391,20 +379,17 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): ) -class RackGroupCSVForm(forms.ModelForm): - site = forms.ModelChoiceField( +class RackGroupCSVForm(CSVModelForm): + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) - parent = forms.ModelChoiceField( + parent = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, to_field_name='name', - help_text='Name of parent rack group', + help_text='Parent rack group', error_messages={ 'invalid_choice': 'Rack group not found.', } @@ -413,10 +398,6 @@ class RackGroupCSVForm(forms.ModelForm): class Meta: model = RackGroup fields = RackGroup.csv_headers - help_texts = { - 'name': 'Name of rack group', - 'slug': 'URL-friendly slug', - } class RackGroupFilterForm(BootstrapMixin, forms.Form): @@ -468,15 +449,14 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): ] -class RackRoleCSVForm(forms.ModelForm): +class RackRoleCSVForm(CSVModelForm): slug = SlugField() class Meta: model = RackRole fields = RackRole.csv_headers help_texts = { - 'name': 'Name of rack role', - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } @@ -527,40 +507,31 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RackCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), - to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + to_field_name='name' ) - group_name = forms.CharField( - help_text='Name of rack group', - required=False + group = CSVModelChoiceField( + queryset=RackGroup.objects.all(), + required=False, + to_field_name='name' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Name of assigned tenant' ) status = CSVChoiceField( choices=RackStatusChoices, required=False, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=RackRole.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned role', - error_messages={ - 'invalid_choice': 'Role not found.', - } + help_text='Name of assigned role' ) type = CSVChoiceField( choices=RackTypeChoices, @@ -580,38 +551,15 @@ class RackCSVForm(CustomFieldModelCSVForm): class Meta: model = Rack fields = Rack.csv_headers - help_texts = { - 'name': 'Rack name', - 'u_height': 'Height in rack units', - } - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - group_name = self.cleaned_data.get('group_name') - name = self.cleaned_data.get('name') - facility_id = self.cleaned_data.get('facility_id') - - # Validate rack group - if group_name: - try: - self.instance.group = RackGroup.objects.get(site=site, name=group_name) - except RackGroup.DoesNotExist: - raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site)) - - # Validate uniqueness of rack name within group - if Rack.objects.filter(group=self.instance.group, name=name).exists(): - raise forms.ValidationError( - "A rack named {} already exists within group {}".format(name, group_name) - ) - - # Validate uniqueness of facility ID within group - if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists(): - raise forms.ValidationError( - "A rack with the facility ID {} already exists within group {}".format(facility_id, group_name) - ) + # Limit group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['group'].queryset = self.fields['group'].queryset.filter(**params) class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -828,62 +776,54 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationCSVForm(forms.ModelForm): - site = forms.ModelChoiceField( +class RackReservationCSVForm(CSVModelForm): + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Invalid site name.', - } + help_text='Parent site' ) - rack_group = forms.CharField( + rack_group = CSVModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, help_text="Rack's group (if any)" ) - rack_name = forms.CharField( - help_text="Rack name" + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', + help_text='Rack' ) units = SimpleArrayField( base_field=forms.IntegerField(), required=True, help_text='Comma-separated list of individual unit numbers' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: model = RackReservation - fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description') - help_texts = { - } + fields = ('site', 'rack_group', 'rack', 'units', 'tenant', 'description') - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit rack_group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if site and rack_group and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group)) - elif site and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site)) + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): @@ -949,15 +889,11 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): ] -class ManufacturerCSVForm(forms.ModelForm): +class ManufacturerCSVForm(CSVModelForm): class Meta: model = Manufacturer fields = Manufacturer.csv_headers - help_texts = { - 'name': 'Manufacturer name', - 'slug': 'URL-friendly slug', - } # @@ -1668,15 +1604,14 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): ] -class DeviceRoleCSVForm(forms.ModelForm): +class DeviceRoleCSVForm(CSVModelForm): slug = SlugField() class Meta: model = DeviceRole fields = DeviceRole.csv_headers help_texts = { - 'name': 'Name of device role', - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } @@ -1703,24 +1638,18 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): } -class PlatformCSVForm(forms.ModelForm): +class PlatformCSVForm(CSVModelForm): slug = SlugField() - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), required=False, to_field_name='name', - help_text='Manufacturer name', - error_messages={ - 'invalid_choice': 'Manufacturer not found.', - } + help_text='Limit platform assignments to this manufacturer' ) class Meta: model = Platform fields = Platform.csv_headers - help_texts = { - 'name': 'Platform name', - } # @@ -1922,173 +1851,131 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class BaseDeviceCSVForm(CustomFieldModelCSVForm): - device_role = forms.ModelChoiceField( + device_role = CSVModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', - help_text='Name of assigned role', - error_messages={ - 'invalid_choice': 'Invalid device role.', - } + help_text='Assigned role' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', - help_text='Device type manufacturer', - error_messages={ - 'invalid_choice': 'Invalid manufacturer.', - } + help_text='Device type manufacturer' ) - model_name = forms.CharField( - help_text='Device type model name' + device_type = CSVModelChoiceField( + queryset=DeviceType.objects.all(), + to_field_name='model', + help_text='Device type model' ) - platform = forms.ModelChoiceField( + platform = CSVModelChoiceField( queryset=Platform.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned platform', - error_messages={ - 'invalid_choice': 'Invalid platform.', - } + help_text='Assigned platform' ) status = CSVChoiceField( choices=DeviceStatusChoices, help_text='Operational status' ) + cluster = CSVModelChoiceField( + queryset=Cluster.objects.all(), + to_field_name='name', + required=False, + help_text='Virtualization cluster' + ) class Meta: fields = [] model = Device - help_texts = { - 'name': 'Device name', - } - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - manufacturer = self.cleaned_data.get('manufacturer') - model_name = self.cleaned_data.get('model_name') - - # Validate device type - if manufacturer and model_name: - try: - self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name) - except DeviceType.DoesNotExist: - raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name)) + # Limit device type queryset by manufacturer + params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')} + self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params) class DeviceCSVForm(BaseDeviceCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Invalid site name.', - } + help_text='Assigned site' ) - rack_group = forms.CharField( + rack_group = CSVModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, - help_text='Parent rack\'s group (if any)' + help_text="Rack's group (if any)" ) - rack_name = forms.CharField( + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', required=False, - help_text='Name of parent rack' + help_text="Assigned rack" ) face = CSVChoiceField( choices=DeviceFaceChoices, required=False, help_text='Mounted rack face' ) - cluster = forms.ModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - required=False, - help_text='Virtualization cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } - ) class Meta(BaseDeviceCSVForm.Meta): fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'site', 'rack_group', 'rack', 'position', 'face', 'cluster', 'comments', ] - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit rack_group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if site and rack_group and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group)) - elif site and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site)) + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class ChildDeviceCSVForm(BaseDeviceCSVForm): - parent = FlexibleModelChoiceField( + parent = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of parent device', - error_messages={ - 'invalid_choice': 'Parent device not found.', - } + help_text='Parent device' ) - device_bay_name = forms.CharField( - help_text='Name of device bay', - ) - cluster = forms.ModelChoiceField( - queryset=Cluster.objects.all(), + device_bay = CSVModelChoiceField( + queryset=Device.objects.all(), to_field_name='name', - required=False, - help_text='Virtualization cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } + help_text='Device bay in which this device is installed' ) class Meta(BaseDeviceCSVForm.Meta): fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', - 'parent', 'device_bay_name', 'cluster', 'comments', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'parent', 'device_bay', 'cluster', 'comments', ] - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - parent = self.cleaned_data.get('parent') - device_bay_name = self.cleaned_data.get('device_bay_name') - - # Validate device bay - if parent and device_bay_name: - try: - self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name) - # Inherit site and rack from parent device - self.instance.site = parent.site - self.instance.rack = parent.rack - except DeviceBay.DoesNotExist: - raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name)) + # Limit device bay queryset by parent device + params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')} + self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -2380,14 +2267,10 @@ class ConsolePortBulkEditForm( ) -class ConsolePortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class ConsolePortCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2484,14 +2367,10 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) -class ConsoleServerPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class ConsoleServerPortCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2584,14 +2463,10 @@ class PowerPortBulkEditForm( ) -class PowerPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class PowerPortCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2735,27 +2610,21 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): ) -class PowerOutletCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class PowerOutletCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) - power_port = FlexibleModelChoiceField( + power_port = CSVModelChoiceField( queryset=PowerPort.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of Power Port', - error_messages={ - 'invalid_choice': 'Power Port not found.', - } + help_text='Local power port which feeds this outlet' ) feed_leg = CSVChoiceField( choices=PowerOutletFeedLegChoices, required=False, + help_text='Electrical phase (for three-phase circuits)' ) class Meta: @@ -3057,40 +2926,31 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): ) -class InterfaceCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class InterfaceCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) - virtual_machine = FlexibleModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, - to_field_name='name', - help_text='Name or ID of virtual machine', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + to_field_name='name' ) - lag = FlexibleModelChoiceField( + lag = CSVModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of LAG interface', - error_messages={ - 'invalid_choice': 'LAG interface not found.', - } + help_text='Parent LAG interface' ) type = CSVChoiceField( choices=InterfaceTypeChoices, + help_text='Physical medium' ) mode = CSVChoiceField( choices=InterfaceModeChoices, required=False, + help_text='IEEE 802.1Q operational mode (for L2 interfaces)' ) class Meta: @@ -3270,30 +3130,27 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): ) -class FrontPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class FrontPortCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) - rear_port = FlexibleModelChoiceField( + rear_port = CSVModelChoiceField( queryset=RearPort.objects.all(), to_field_name='name', - help_text='Name or ID of Rear Port', - error_messages={ - 'invalid_choice': 'Rear Port not found.', - } + help_text='Corresponding rear port' ) type = CSVChoiceField( choices=PortTypeChoices, + help_text='Physical medium classification' ) class Meta: model = FrontPort fields = FrontPort.csv_headers + help_texts = { + 'rear_port_position': 'Mapped position on corresponding rear port', + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -3408,22 +3265,22 @@ class RearPortBulkDisconnectForm(ConfirmationForm): ) -class RearPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class RearPortCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) type = CSVChoiceField( + help_text='Physical medium classification', choices=PortTypeChoices, ) class Meta: model = RearPort fields = RearPort.csv_headers + help_texts = { + 'positions': 'Number of front ports which may be mapped' + } # @@ -3516,20 +3373,16 @@ class DeviceBayBulkRenameForm(BulkRenameForm): ) -class DeviceBayCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class DeviceBayCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) - installed_device = FlexibleModelChoiceField( + installed_device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of device', + help_text='Child device installed within this bay', error_messages={ 'invalid_choice': 'Child device not found.', } @@ -3808,44 +3661,37 @@ class CableForm(BootstrapMixin, forms.ModelForm): } -class CableCSVForm(forms.ModelForm): - +class CableCSVForm(CSVModelForm): # Termination A - side_a_device = FlexibleModelChoiceField( + side_a_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side A device name or ID', - error_messages={ - 'invalid_choice': 'Side A device not found', - } + help_text='Side A device' ) - side_a_type = forms.ModelChoiceField( + side_a_type = CSVModelChoiceField( queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side A type' ) side_a_name = forms.CharField( - help_text='Side A component' + help_text='Side A component name' ) # Termination B - side_b_device = FlexibleModelChoiceField( + side_b_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side B device name or ID', - error_messages={ - 'invalid_choice': 'Side B device not found', - } + help_text='Side B device' ) - side_b_type = forms.ModelChoiceField( + side_b_type = CSVModelChoiceField( queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side B type' ) side_b_name = forms.CharField( - help_text='Side B component' + help_text='Side B component name' ) # Cable attributes @@ -3857,7 +3703,7 @@ class CableCSVForm(forms.ModelForm): type = CSVChoiceField( choices=CableTypeChoices, required=False, - help_text='Cable type' + help_text='Physical medium classification' ) length_unit = CSVChoiceField( choices=CableLengthUnitChoices, @@ -3872,7 +3718,7 @@ class CableCSVForm(forms.ModelForm): 'status', 'label', 'color', 'length', 'length_unit', ] help_texts = { - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } # TODO: Merge the clean() methods for either end @@ -4163,23 +4009,15 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form): ) -class InventoryItemCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( +class InventoryItemCSVForm(CSVModelForm): + device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text='Device name or ID', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', - required=False, - help_text='Manufacturer name', - error_messages={ - 'invalid_choice': 'Invalid manufacturer.', - } + required=False ) class Meta: @@ -4388,6 +4226,20 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): return device +class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=VirtualChassis.objects.all(), + widget=forms.MultipleHiddenInput() + ) + domain = forms.CharField( + max_length=30, + required=False + ) + + class Meta: + nullable_fields = ['domain'] + + class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VirtualChassis q = forms.CharField( @@ -4462,39 +4314,30 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): ] -class PowerPanelCSVForm(forms.ModelForm): - site = forms.ModelChoiceField( +class PowerPanelCSVForm(CSVModelForm): + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Name of parent site' ) - rack_group_name = forms.CharField( + rack_group = CSVModelChoiceField( + queryset=RackGroup.objects.all(), required=False, - help_text="Rack group name (optional)" + to_field_name='name' ) class Meta: model = PowerPanel fields = PowerPanel.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group_name = self.cleaned_data.get('rack_group_name') - - # Validate rack group - if rack_group_name: - try: - self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name) - except RackGroup.DoesNotExist: - raise forms.ValidationError( - "Rack group {} not found in site {}".format(rack_group_name, site) - ) + # Limit group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm): @@ -4610,29 +4453,27 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): class PowerFeedCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) - panel_name = forms.ModelChoiceField( + power_panel = CSVModelChoiceField( queryset=PowerPanel.objects.all(), to_field_name='name', - help_text='Name of upstream power panel', - error_messages={ - 'invalid_choice': 'Power panel not found.', - } + help_text='Upstream power panel' ) - rack_group = forms.CharField( + rack_group = CSVModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, - help_text="Rack group name (optional)" + help_text="Rack's group (if any)" ) - rack_name = forms.CharField( + rack = CSVModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', required=False, - help_text="Rack name (optional)" + help_text='Rack' ) status = CSVChoiceField( choices=PowerFeedStatusChoices, @@ -4647,7 +4488,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): supply = CSVChoiceField( choices=PowerFeedSupplyChoices, required=False, - help_text='AC/DC' + help_text='Supply type (AC/DC)' ) phase = CSVChoiceField( choices=PowerFeedPhaseChoices, @@ -4659,32 +4500,25 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): model = PowerFeed fields = PowerFeed.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - panel_name = self.cleaned_data.get('panel_name') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit power_panel queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params) - # Validate power panel - if panel_name: - try: - self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name) - except Rack.DoesNotExist: - raise forms.ValidationError( - "Power panel {} not found in site {}".format(panel_name, site) - ) + # Limit rack_group queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError( - "Rack {} not found in site {}, group {}".format(rack_name, site, rack_group) - ) + # Limit rack queryset by site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): diff --git a/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py b/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py deleted file mode 100644 index a9f80f49b..000000000 --- a/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py +++ /dev/null @@ -1,101 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - -import dcim.fields - - -def copy_primary_ip(apps, schema_editor): - Device = apps.get_model('dcim', 'Device') - for d in Device.objects.select_related('primary_ip'): - if not d.primary_ip: - continue - if d.primary_ip.family == 4: - d.primary_ip4 = d.primary_ip - elif d.primary_ip.family == 6: - d.primary_ip6 = d.primary_ip - d.save() - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null')] - - dependencies = [ - ('ipam', '0001_initial'), - ('dcim', '0002_auto_20160622_1821'), - ] - - operations = [ - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), - ), - migrations.AddField( - model_name='devicetype', - name='subdevice_role', - field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'), - ), - migrations.CreateModel( - name='DeviceBayTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30)), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')), - ], - options={ - 'ordering': ['device_type', 'name'], - 'unique_together': {('device_type', 'name')}, - }, - ), - migrations.CreateModel( - name='DeviceBay', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name=b'Name')), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')), - ('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')), - ], - options={ - 'ordering': ['device', 'name'], - 'unique_together': {('device', 'name')}, - }, - ), - migrations.AddField( - model_name='interface', - name='mac_address', - field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'), - ), - migrations.AddField( - model_name='device', - name='primary_ip4', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'), - ), - migrations.AddField( - model_name='device', - name='primary_ip6', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'), - ), - migrations.RunPython( - code=copy_primary_ip, - ), - migrations.RemoveField( - model_name='device', - name='primary_ip', - ), - migrations.AlterField( - model_name='site', - name='asn', - field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'), - ), - migrations.AlterField( - model_name='devicebay', - name='installed_device', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'), - ), - ] diff --git a/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py b/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py deleted file mode 100644 index dac983398..000000000 --- a/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py +++ /dev/null @@ -1,154 +0,0 @@ -import django.core.validators -import django.db.models.deletion -from django.db import migrations, models - -import utilities.fields - -COLOR_CONVERSION = { - 'teal': '009688', - 'green': '4caf50', - 'blue': '2196f3', - 'purple': '9c27b0', - 'yellow': 'ffeb3b', - 'orange': 'ff9800', - 'red': 'f44336', - 'light_gray': 'c0c0c0', - 'medium_gray': '9e9e9e', - 'dark_gray': '607d8b', -} - - -def color_names_to_rgb(apps, schema_editor): - RackRole = apps.get_model('dcim', 'RackRole') - DeviceRole = apps.get_model('dcim', 'DeviceRole') - for color_name, color_rgb in COLOR_CONVERSION.items(): - RackRole.objects.filter(color=color_name).update(color=color_rgb) - DeviceRole.objects.filter(color=color_name).update(color=color_rgb) - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')] - - dependencies = [ - ('dcim', '0010_devicebay_installed_device_set_null'), - ('tenancy', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='devicetype', - name='part_number', - field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50), - ), - migrations.AddField( - model_name='device', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='rack', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='site', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), - ), - migrations.AddField( - model_name='rack', - name='type', - field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'), - ), - migrations.AddField( - model_name='rack', - name='width', - field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'), - ), - migrations.AlterField( - model_name='rack', - name='u_height', - field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'), - ), - migrations.AddField( - model_name='module', - name='manufacturer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'), - ), - migrations.CreateModel( - name='RackRole', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.AddField( - model_name='rack', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'), - ), - migrations.AddField( - model_name='device', - name='asset_tag', - field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AddField( - model_name='rack', - name='desc_units', - field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'), - ), - migrations.AlterField( - model_name='device', - name='position', - field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.RunPython( - code=color_names_to_rgb, - ), - migrations.AlterField( - model_name='devicerole', - name='color', - field=utilities.fields.ColorField(max_length=6), - ), - migrations.AlterField( - model_name='rackrole', - name='color', - field=utilities.fields.ColorField(max_length=6), - ), - ] diff --git a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py b/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py deleted file mode 100644 index 064832e80..000000000 --- a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py +++ /dev/null @@ -1,478 +0,0 @@ -import django.contrib.postgres.fields -import django.core.validators -import django.db.models.deletion -import mptt.fields -from django.conf import settings -from django.db import migrations, models - -import dcim.fields -import utilities.fields - - -def copy_site_from_rack(apps, schema_editor): - Device = apps.get_model('dcim', 'Device') - for device in Device.objects.all(): - device.site = device.rack.site - device.save() - - -def rpc_client_to_napalm_driver(apps, schema_editor): - """ - Migrate legacy RPC clients to their respective NAPALM drivers - """ - Platform = apps.get_model('dcim', 'Platform') - - Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos') - Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios') - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0023_devicetype_comments'), ('dcim', '0024_site_add_contact_fields'), ('dcim', '0025_devicetype_add_interface_ordering'), ('dcim', '0026_add_rack_reservations'), ('dcim', '0027_device_add_site'), ('dcim', '0028_device_copy_rack_to_site'), ('dcim', '0029_allow_rackless_devices'), ('dcim', '0030_interface_add_lag'), ('dcim', '0031_regions'), ('dcim', '0032_device_increase_name_length'), ('dcim', '0033_rackreservation_rack_editable'), ('dcim', '0034_rename_module_to_inventoryitem'), ('dcim', '0035_device_expand_status_choices'), ('dcim', '0036_add_ff_juniper_vcp'), ('dcim', '0037_unicode_literals'), ('dcim', '0038_wireless_interfaces'), ('dcim', '0039_interface_add_enabled_mtu'), ('dcim', '0040_inventoryitem_add_asset_tag_description'), ('dcim', '0041_napalm_integration'), ('dcim', '0042_interface_ff_10ge_cx4'), ('dcim', '0043_device_component_name_lengths')] - - dependencies = [ - ('dcim', '0022_color_names_to_rgb'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='devicetype', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='site', - name='contact_email', - field=models.EmailField(blank=True, max_length=254, verbose_name=b'Contact E-mail'), - ), - migrations.AddField( - model_name='site', - name='contact_name', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='site', - name='contact_phone', - field=models.CharField(blank=True, max_length=20), - ), - migrations.AddField( - model_name='devicetype', - name='interface_ordering', - field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1), - ), - migrations.CreateModel( - name='RackReservation', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)), - ('created', models.DateTimeField(auto_now_add=True)), - ('description', models.CharField(max_length=100)), - ('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')), - ('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['created'], - }, - ), - migrations.AddField( - model_name='device', - name='site', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'), - ), - migrations.RunPython( - code=copy_site_from_rack, - ), - migrations.AlterField( - model_name='device', - name='rack', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'), - ), - migrations.AlterField( - model_name='device', - name='site', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.CreateModel( - name='Region', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='site', - name='region', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'), - ), - migrations.AlterField( - model_name='device', - name='name', - field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True), - ), - migrations.AlterField( - model_name='rackreservation', - name='rack', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'), - ), - migrations.RenameModel( - old_name='Module', - new_name='InventoryItem', - ), - migrations.AlterField( - model_name='inventoryitem', - name='device', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='manufacturer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'), - ), - migrations.AlterField( - model_name='device', - name='status', - field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'), - ), - migrations.AlterField( - model_name='device', - name='status', - field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='consoleport', - name='connection_status', - field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), - ), - migrations.AlterField( - model_name='consoleport', - name='cs_port', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'), - ), - migrations.AlterField( - model_name='device', - name='asset_tag', - field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'), - ), - migrations.AlterField( - model_name='device', - name='face', - field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'), - ), - migrations.AlterField( - model_name='device', - name='position', - field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'), - ), - migrations.AlterField( - model_name='device', - name='primary_ip4', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'), - ), - migrations.AlterField( - model_name='device', - name='primary_ip6', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'), - ), - migrations.AlterField( - model_name='device', - name='serial', - field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), - ), - migrations.AlterField( - model_name='device', - name='status', - field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), - ), - migrations.AlterField( - model_name='devicebay', - name='name', - field=models.CharField(max_length=50, verbose_name='Name'), - ), - migrations.AlterField( - model_name='devicetype', - name='interface_ordering', - field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1), - ), - migrations.AlterField( - model_name='devicetype', - name='is_console_server', - field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'), - ), - migrations.AlterField( - model_name='devicetype', - name='is_full_depth', - field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'), - ), - migrations.AlterField( - model_name='devicetype', - name='is_network_device', - field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'), - ), - migrations.AlterField( - model_name='devicetype', - name='is_pdu', - field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'), - ), - migrations.AlterField( - model_name='devicetype', - name='part_number', - field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50), - ), - migrations.AlterField( - model_name='devicetype', - name='subdevice_role', - field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'), - ), - migrations.AlterField( - model_name='devicetype', - name='u_height', - field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AddField( - model_name='interface', - name='lag', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), - ), - migrations.AlterField( - model_name='interface', - name='mac_address', - field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'), - ), - migrations.AlterField( - model_name='interface', - name='mgmt_only', - field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'), - ), - migrations.AlterField( - model_name='interfaceconnection', - name='connection_status', - field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='mgmt_only', - field=models.BooleanField(default=False, verbose_name='Management only'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='discovered', - field=models.BooleanField(default=False, verbose_name='Discovered'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='name', - field=models.CharField(max_length=50, verbose_name='Name'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='part_id', - field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='serial', - field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), - ), - migrations.AlterField( - model_name='platform', - name='rpc_client', - field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'), - ), - migrations.AlterField( - model_name='powerport', - name='connection_status', - field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), - ), - migrations.AlterField( - model_name='rack', - name='desc_units', - field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'), - ), - migrations.AlterField( - model_name='rack', - name='facility_id', - field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'), - ), - migrations.AlterField( - model_name='rack', - name='type', - field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'), - ), - migrations.AlterField( - model_name='rack', - name='u_height', - field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'), - ), - migrations.AlterField( - model_name='rack', - name='width', - field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'), - ), - migrations.AlterField( - model_name='site', - name='asn', - field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), - ), - migrations.AlterField( - model_name='site', - name='contact_email', - field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AddField( - model_name='interface', - name='enabled', - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name='interface', - name='mtu', - field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'), - ), - migrations.AddField( - model_name='inventoryitem', - name='asset_tag', - field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'), - ), - migrations.AddField( - model_name='inventoryitem', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AlterModelOptions( - name='device', - options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))}, - ), - migrations.AddField( - model_name='platform', - name='napalm_driver', - field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'), - ), - migrations.AlterField( - model_name='platform', - name='rpc_client', - field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'), - ), - migrations.RunPython( - code=rpc_client_to_napalm_driver, - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='consoleport', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='consoleserverport', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='devicebaytemplate', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='interface', - name='name', - field=models.CharField(max_length=64), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='name', - field=models.CharField(max_length=64), - ), - migrations.AlterField( - model_name='poweroutlet', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='powerport', - name='name', - field=models.CharField(max_length=50), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='name', - field=models.CharField(max_length=50), - ), - ] diff --git a/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py b/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py deleted file mode 100644 index 18ef39fe7..000000000 --- a/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py +++ /dev/null @@ -1,354 +0,0 @@ -import django.contrib.postgres.fields.jsonb -import django.core.validators -import django.db.models.deletion -import taggit.managers -import timezone_field.fields -from django.conf import settings -from django.db import migrations, models - -import utilities.fields - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering'), ('dcim', '0056_django2'), ('dcim', '0057_tags'), ('dcim', '0058_relax_rack_naming_constraints'), ('dcim', '0059_site_latitude_longitude'), ('dcim', '0060_change_logging'), ('dcim', '0061_platform_napalm_args')] - - dependencies = [ - ('virtualization', '0001_virtualization'), - ('tenancy', '0003_unicode_literals'), - ('ipam', '0020_ipaddress_add_role_carp'), - ('dcim', '0043_device_component_name_lengths'), - ('taggit', '0002_auto_20150616_2121'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='cluster', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'), - ), - migrations.AddField( - model_name='interface', - name='virtual_machine', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'), - ), - migrations.AlterField( - model_name='interface', - name='device', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'), - ), - migrations.AddField( - model_name='devicerole', - name='vm_role', - field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'), - ), - migrations.AlterField( - model_name='rack', - name='facility_id', - field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AddField( - model_name='rack', - name='serial', - field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), - ), - migrations.AlterField( - model_name='rackreservation', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='interface', - name='mode', - field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True), - ), - migrations.AddField( - model_name='interface', - name='tagged_vlans', - field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'), - ), - migrations.AddField( - model_name='rackreservation', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'), - ), - migrations.CreateModel( - name='VirtualChassis', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('domain', models.CharField(blank=True, max_length=30)), - ('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')), - ], - options={ - 'ordering': ['master'], - 'verbose_name_plural': 'virtual chassis', - }, - ), - migrations.AddField( - model_name='device', - name='virtual_chassis', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'), - ), - migrations.AddField( - model_name='device', - name='vc_position', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), - ), - migrations.AddField( - model_name='device', - name='vc_priority', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), - ), - migrations.AlterUniqueTogether( - name='device', - unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')}, - ), - migrations.AlterField( - model_name='platform', - name='napalm_driver', - field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'), - ), - migrations.AddField( - model_name='site', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AddField( - model_name='site', - name='status', - field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1), - ), - migrations.AddField( - model_name='site', - name='time_zone', - field=timezone_field.fields.TimeZoneField(blank=True), - ), - migrations.AlterField( - model_name='virtualchassis', - name='master', - field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'), - ), - migrations.AddField( - model_name='interface', - name='untagged_vlan', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'), - ), - migrations.AddField( - model_name='platform', - name='manufacturer', - field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='dcim.Manufacturer'), - ), - migrations.AddField( - model_name='device', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='devicetype', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='rack', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='site', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='consoleport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='consoleserverport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='devicebay', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='interface', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='inventoryitem', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='poweroutlet', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='powerport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='virtualchassis', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), - ), - migrations.AlterModelOptions( - name='rack', - options={'ordering': ['site', 'group', 'name']}, - ), - migrations.AlterUniqueTogether( - name='rack', - unique_together={('group', 'name'), ('group', 'facility_id')}, - ), - migrations.AddField( - model_name='site', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), - ), - migrations.AddField( - model_name='site', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), - ), - migrations.AddField( - model_name='devicerole', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='devicerole', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='devicetype', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='devicetype', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='manufacturer', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='manufacturer', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='platform', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='platform', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='rackgroup', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='rackgroup', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='rackreservation', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='rackrole', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='rackrole', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='region', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='region', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='virtualchassis', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='virtualchassis', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='device', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='device', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='rack', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rack', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='rackreservation', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='site', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='site', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='platform', - name='napalm_args', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'), - ), - ] diff --git a/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py b/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py deleted file mode 100644 index 71ce4191f..000000000 --- a/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py +++ /dev/null @@ -1,124 +0,0 @@ -import django.contrib.postgres.fields.jsonb -import django.core.validators -import django.db.models.deletion -import taggit.managers -from django.db import migrations, models - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0062_interface_mtu'), ('dcim', '0063_device_local_context_data'), ('dcim', '0064_remove_platform_rpc_client'), ('dcim', '0065_front_rear_ports')] - - dependencies = [ - ('taggit', '0002_auto_20150616_2121'), - ('dcim', '0061_platform_napalm_args'), - ] - - operations = [ - migrations.AlterField( - model_name='interface', - name='mtu', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AddField( - model_name='device', - name='local_context_data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), - ), - migrations.RemoveField( - model_name='platform', - name='rpc_client', - ), - migrations.CreateModel( - name='RearPort', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('type', models.PositiveSmallIntegerField()), - ('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), - ('description', models.CharField(blank=True, max_length=100)), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.Device')), - ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), - ], - options={ - 'ordering': ['device', 'name'], - 'unique_together': {('device', 'name')}, - }, - ), - migrations.CreateModel( - name='RearPortTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('type', models.PositiveSmallIntegerField()), - ('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearport_templates', to='dcim.DeviceType')), - ], - options={ - 'ordering': ['device_type', 'name'], - 'unique_together': {('device_type', 'name')}, - }, - ), - migrations.CreateModel( - name='FrontPortTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('type', models.PositiveSmallIntegerField()), - ('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.DeviceType')), - ('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.RearPortTemplate')), - ], - options={ - 'ordering': ['device_type', 'name'], - 'unique_together': {('rear_port', 'rear_port_position'), ('device_type', 'name')}, - }, - ), - migrations.CreateModel( - name='FrontPort', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('type', models.PositiveSmallIntegerField()), - ('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), - ('description', models.CharField(blank=True, max_length=100)), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.Device')), - ('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.RearPort')), - ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), - ], - options={ - 'ordering': ['device', 'name'], - 'unique_together': {('device', 'name'), ('rear_port', 'rear_port_position')}, - }, - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleport_templates', to='dcim.DeviceType'), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverport_templates', to='dcim.DeviceType'), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlet_templates', to='dcim.DeviceType'), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerport_templates', to='dcim.DeviceType'), - ), - ] diff --git a/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py b/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py deleted file mode 100644 index 6fbf115d9..000000000 --- a/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py +++ /dev/null @@ -1,146 +0,0 @@ -import taggit.managers -from django.db import migrations, models - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0067_device_type_remove_qualifiers'), ('dcim', '0068_rack_new_fields'), ('dcim', '0069_deprecate_nullablecharfield'), ('dcim', '0070_custom_tag_models')] - - dependencies = [ - ('extras', '0019_tag_taggeditem'), - ('dcim', '0066_cables'), - ] - - operations = [ - migrations.RemoveField( - model_name='devicetype', - name='is_console_server', - ), - migrations.RemoveField( - model_name='devicetype', - name='is_network_device', - ), - migrations.RemoveField( - model_name='devicetype', - name='is_pdu', - ), - migrations.RemoveField( - model_name='devicetype', - name='interface_ordering', - ), - migrations.AddField( - model_name='rack', - name='status', - field=models.PositiveSmallIntegerField(default=3), - ), - migrations.AddField( - model_name='rack', - name='outer_depth', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='rack', - name='outer_unit', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='rack', - name='outer_width', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='device', - name='asset_tag', - field=models.CharField(blank=True, max_length=50, null=True, unique=True), - ), - migrations.AlterField( - model_name='device', - name='name', - field=models.CharField(blank=True, max_length=64, null=True, unique=True), - ), - migrations.AlterField( - model_name='inventoryitem', - name='asset_tag', - field=models.CharField(blank=True, max_length=50, null=True, unique=True), - ), - migrations.AddField( - model_name='rack', - name='asset_tag', - field=models.CharField(blank=True, max_length=50, null=True, unique=True), - ), - migrations.AlterField( - model_name='rack', - name='facility_id', - field=models.CharField(blank=True, max_length=50, null=True), - ), - migrations.AlterField( - model_name='consoleport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='consoleserverport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='device', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='devicebay', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='devicetype', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='frontport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='interface', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='inventoryitem', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='poweroutlet', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='powerport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='rack', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='rearport', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='site', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AlterField( - model_name='virtualchassis', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - ] diff --git a/netbox/dcim/migrations/0092_fix_rack_outer_unit.py b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py index 2a8cbf4e5..3d63f1cb3 100644 --- a/netbox/dcim/migrations/0092_fix_rack_outer_unit.py +++ b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py @@ -19,8 +19,7 @@ class Migration(migrations.Migration): ] 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. + # Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed. migrations.RunPython( code=rack_outer_unit_to_slug ), diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 096065cab..b0da352da 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -12,6 +12,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Count, F, ProtectedError, Sum from django.urls import reverse +from django.utils.safestring import mark_safe from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from timezone_field import TimeZoneField @@ -179,12 +180,14 @@ class Site(ChangeLoggedModel, CustomFieldModel): ) facility = models.CharField( max_length=50, - blank=True + blank=True, + help_text='Local facility ID or description' ) asn = ASNField( blank=True, null=True, - verbose_name='ASN' + verbose_name='ASN', + help_text='32-bit autonomous system number' ) time_zone = TimeZoneField( blank=True @@ -205,13 +208,15 @@ class Site(ChangeLoggedModel, CustomFieldModel): max_digits=8, decimal_places=6, blank=True, - null=True + null=True, + help_text='GPS coordinate (latitude)' ) longitude = models.DecimalField( max_digits=9, decimal_places=6, blank=True, - null=True + null=True, + help_text='GPS coordinate (longitude)' ) contact_name = models.CharField( max_length=50, @@ -418,7 +423,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): max_length=50, blank=True, null=True, - verbose_name='Facility ID' + verbose_name='Facility ID', + help_text='Locally-assigned identifier' ) site = models.ForeignKey( to='dcim.Site', @@ -430,7 +436,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): on_delete=models.SET_NULL, related_name='racks', blank=True, - null=True + null=True, + help_text='Assigned group' ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -449,7 +456,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): on_delete=models.PROTECT, related_name='racks', blank=True, - null=True + null=True, + help_text='Functional role' ) serial = models.CharField( max_length=50, @@ -479,7 +487,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): u_height = models.PositiveSmallIntegerField( default=RACK_U_HEIGHT_DEFAULT, verbose_name='Height (U)', - validators=[MinValueValidator(1), MaxValueValidator(100)] + validators=[MinValueValidator(1), MaxValueValidator(100)], + help_text='Height in rack units' ) desc_units = models.BooleanField( default=False, @@ -488,11 +497,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel): ) outer_width = models.PositiveSmallIntegerField( blank=True, - null=True + null=True, + help_text='Outer dimension of rack (width)' ) outer_depth = models.PositiveSmallIntegerField( blank=True, - null=True + null=True, + help_text='Outer dimension of rack (depth)' ) outer_unit = models.CharField( max_length=50, @@ -513,7 +524,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', + 'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] clone_fields = [ @@ -652,7 +663,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): pk=exclude ).filter( rack=self, - position__gt=0 + position__gt=0, + device_type__u_height__gt=0 ).filter( Q(face=face) | Q(device_type__is_full_depth=True) ) @@ -819,7 +831,7 @@ class RackReservation(ChangeLoggedModel): def clean(self): - if self.units: + if hasattr(self, 'rack') and self.units: # Validate that all specified units exist in the Rack. invalid_units = [u for u in self.units if u not in self.rack.units] @@ -1089,17 +1101,32 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have # room to expand within their racks. This validation will impose a very high performance penalty when there are # many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence. - if self.pk is not None and self.u_height > self._original_u_height: + if self.pk and self.u_height > self._original_u_height: for d in Device.objects.filter(device_type=self, position__isnull=False): face_required = None if self.is_full_depth else d.face - u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required, - exclude=[d.pk]) + u_available = d.rack.get_available_units( + u_height=self.u_height, + rack_face=face_required, + exclude=[d.pk] + ) if d.position not in u_available: raise ValidationError({ 'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of " "{}U".format(d, d.rack, self.u_height) }) + # If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position. + elif self.pk and self._original_u_height > 0 and self.u_height == 0: + racked_instance_count = Device.objects.filter(device_type=self, position__isnull=False).count() + if racked_instance_count: + url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}" + raise ValidationError({ + 'u_height': mark_safe( + f'Unable to set 0U height: Found {racked_instance_count} instances already ' + f'mounted within racks.' + ) + }) + if ( self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT ) and self.device_bay_templates.count(): @@ -1398,7 +1425,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] clone_fields = [ @@ -1695,7 +1722,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): # Virtual chassis # -@extras_features('export_templates', 'webhooks') +@extras_features('custom_links', 'export_templates', 'webhooks') class VirtualChassis(ChangeLoggedModel): """ A collection of Devices which operate with a shared control plane (e.g. a switch stack). @@ -1722,7 +1749,7 @@ class VirtualChassis(ChangeLoggedModel): return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis' def get_absolute_url(self): - return self.master.get_absolute_url() + return reverse('dcim:virtualchassis', kwargs={'pk': self.pk}) def clean(self): @@ -1781,7 +1808,7 @@ class PowerPanel(ChangeLoggedModel): max_length=50 ) - csv_headers = ['site', 'rack_group_name', 'name'] + csv_headers = ['site', 'rack_group', 'name'] class Meta: ordering = ['site', 'name'] @@ -1888,7 +1915,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', + 'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', ] clone_fields = [ diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8c79d89d8..4005d41a4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -123,11 +123,12 @@ class CableTermination(models.Model): # Map a rear port/position to its corresponding front port elif isinstance(termination, RearPort): - # Can't map to a FrontPort without a position - if not position_stack: + # Can't map to a FrontPort without a position if there are multiple options + if termination.positions > 1 and not position_stack: raise CableTraceSplit(termination) - position = position_stack.pop() + # We can assume position 1 if the RearPort has only one position + position = position_stack.pop() if position_stack else 1 # Validate the position if position not in range(1, termination.positions + 1): @@ -238,7 +239,8 @@ class ConsolePort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) connected_endpoint = models.OneToOneField( to='dcim.ConsoleServerPort', @@ -299,7 +301,8 @@ class ConsoleServerPort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) connection_status = models.NullBooleanField( choices=CONNECTION_STATUS_CHOICES, @@ -353,7 +356,8 @@ class PowerPort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=PowerPortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) maximum_draw = models.PositiveSmallIntegerField( blank=True, @@ -515,7 +519,8 @@ class PowerOutlet(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=PowerOutletTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) power_port = models.ForeignKey( to='dcim.PowerPort', @@ -652,7 +657,7 @@ class Interface(CableTermination, ComponentModel): mode = models.CharField( max_length=50, choices=InterfaceModeChoices, - blank=True, + blank=True ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', @@ -1082,7 +1087,8 @@ class InventoryItem(ComponentModel): part_id = models.CharField( max_length=50, verbose_name='Part ID', - blank=True + blank=True, + help_text='Manufacturer-assigned part identifier' ) serial = models.CharField( max_length=50, @@ -1099,7 +1105,7 @@ class InventoryItem(ComponentModel): ) discovered = models.BooleanField( default=False, - verbose_name='Discovered' + help_text='This item was automatically discovered' ) tags = TaggableManager(through=TaggedItem) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 7131a6be3..3fef86394 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from tenancy.tables import COL_TENANT -from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn +from utilities.tables import BaseTable, BooleanColumn, ColorColumn, TagColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -165,15 +165,6 @@ UTILIZATION_GRAPH = """ {% utilization_graph value %} """ -VIRTUALCHASSIS_ACTIONS = """ - - - -{% if perms.dcim.change_virtualchassis %} - -{% endif %} -""" - CABLE_TERMINATION_PARENT = """ {% if value.device %} {{ value.device }} @@ -214,9 +205,13 @@ def get_component_template_actions(model_name): class RegionTable(BaseTable): pk = ToggleColumn() - name = tables.TemplateColumn(template_code=MPTT_LINK, orderable=False) - site_count = tables.Column(verbose_name='Sites') - slug = tables.Column(verbose_name='Slug') + name = tables.TemplateColumn( + template_code=MPTT_LINK, + orderable=False + ) + site_count = tables.Column( + verbose_name='Sites' + ) actions = tables.TemplateColumn( template_code=REGION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -225,7 +220,8 @@ class RegionTable(BaseTable): class Meta(BaseTable.Meta): model = Region - fields = ('pk', 'name', 'site_count', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') + default_columns = ('pk', 'name', 'site_count', 'description', 'actions') # @@ -234,14 +230,30 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_name',)) - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + name = tables.LinkColumn( + order_by=('_name',) + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + region = tables.TemplateColumn( + template_code=SITE_REGION_LINK + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + tags = TagColumn( + url_name='dcim:site_list' + ) class Meta(BaseTable.Meta): model = Site - fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description') + fields = ( + 'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'tags', + ) + default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description') # @@ -262,7 +274,6 @@ class RackGroupTable(BaseTable): rack_count = tables.Column( verbose_name='Racks' ) - slug = tables.Column() actions = tables.TemplateColumn( template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -272,6 +283,7 @@ class RackGroupTable(BaseTable): class Meta(BaseTable.Meta): model = RackGroup fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions') # @@ -291,6 +303,7 @@ class RackRoleTable(BaseTable): class Meta(BaseTable.Meta): model = RackRole fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') # @@ -299,17 +312,34 @@ class RackRoleTable(BaseTable): class RackTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_name',)) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(RACK_ROLE) - u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') + name = tables.LinkColumn( + order_by=('_name',) + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=RACK_ROLE + ) + u_height = tables.TemplateColumn( + template_code="{{ record.u_height }}U", + verbose_name='Height' + ) class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') + fields = ( + 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'width', 'u_height', + ) + default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') class RackDetailTable(RackTable): @@ -327,9 +357,16 @@ class RackDetailTable(RackTable): orderable=False, verbose_name='Power' ) + tags = TagColumn( + url_name='dcim:rack_list' + ) class Meta(RackTable.Meta): fields = ( + 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', + ) + default_columns = ( 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', ) @@ -373,6 +410,9 @@ class RackReservationTable(BaseTable): fields = ( 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions', ) + default_columns = ( + 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions', + ) # @@ -416,17 +456,25 @@ class DeviceTypeTable(BaseTable): args=[Accessor('pk')], verbose_name='Device Type' ) - is_full_depth = BooleanColumn(verbose_name='Full Depth') + is_full_depth = BooleanColumn( + verbose_name='Full Depth' + ) instance_count = tables.TemplateColumn( template_code=DEVICETYPE_INSTANCES_TEMPLATE, verbose_name='Instances' ) + tags = TagColumn( + url_name='dcim:devicetype_list' + ) class Meta(BaseTable.Meta): model = DeviceType fields = ( - 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', - 'instance_count', + 'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'instance_count', 'tags', + ) + default_columns = ( + 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', ) @@ -436,7 +484,9 @@ class DeviceTypeTable(BaseTable): class ConsolePortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -450,7 +500,10 @@ class ConsolePortTemplateTable(BaseTable): class ConsolePortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = ConsolePort @@ -460,7 +513,9 @@ class ConsolePortImportTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleserverporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -474,7 +529,10 @@ class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = ConsoleServerPort @@ -484,7 +542,9 @@ class ConsoleServerPortImportTable(BaseTable): class PowerPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('powerporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -498,7 +558,10 @@ class PowerPortTemplateTable(BaseTable): class PowerPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = PowerPort @@ -508,7 +571,9 @@ class PowerPortImportTable(BaseTable): class PowerOutletTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('poweroutlettemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -522,7 +587,10 @@ class PowerOutletTemplateTable(BaseTable): class PowerOutletImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = PowerOutlet @@ -532,7 +600,9 @@ class PowerOutletImportTable(BaseTable): class InterfaceTemplateTable(BaseTable): pk = ToggleColumn() - mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") + mgmt_only = tables.TemplateColumn( + template_code="{% if value %}OOB Management{% endif %}" + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('interfacetemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -546,18 +616,30 @@ class InterfaceTemplateTable(BaseTable): class InterfaceImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') - virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) + virtual_machine = tables.LinkColumn( + viewname='virtualization:virtualmachine', + args=[Accessor('virtual_machine.pk')], + verbose_name='Virtual Machine' + ) class Meta(BaseTable.Meta): model = Interface - fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode') + fields = ( + 'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', + 'mgmt_only', 'mode', + ) empty_text = False class FrontPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) rear_port_position = tables.Column( verbose_name='Position' ) @@ -574,7 +656,10 @@ class FrontPortTemplateTable(BaseTable): class FrontPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = FrontPort @@ -584,7 +669,9 @@ class FrontPortImportTable(BaseTable): class RearPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('rearporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -598,7 +685,10 @@ class RearPortTemplateTable(BaseTable): class RearPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = RearPort @@ -608,7 +698,9 @@ class RearPortImportTable(BaseTable): class DeviceBayTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('devicebaytemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -639,8 +731,10 @@ class DeviceRoleTable(BaseTable): orderable=False, verbose_name='VMs' ) - color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label') - slug = tables.Column(verbose_name='Slug') + color = tables.TemplateColumn( + template_code=COLOR_LABEL, + verbose_name='Label' + ) actions = tables.TemplateColumn( template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -650,6 +744,7 @@ class DeviceRoleTable(BaseTable): class Meta(BaseTable.Meta): model = DeviceRole fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') # @@ -679,7 +774,11 @@ class PlatformTable(BaseTable): class Meta(BaseTable.Meta): model = Platform fields = ( - 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'description', 'actions', + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', + 'description', 'actions', + ) + default_columns = ( + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', ) @@ -693,40 +792,99 @@ class DeviceTable(BaseTable): order_by=('_name',), template_code=DEVICE_LINK ) - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) - device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + args=[Accessor('rack.pk')] + ) + device_role = tables.TemplateColumn( + template_code=DEVICE_ROLE, + verbose_name='Role' + ) device_type = tables.LinkColumn( - 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + viewname='dcim:devicetype', + args=[Accessor('device_type.pk')], + verbose_name='Type', text=lambda record: record.device_type.display_name ) + primary_ip = tables.TemplateColumn( + template_code=DEVICE_PRIMARY_IP, + orderable=False, + verbose_name='IP Address' + ) + primary_ip4 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip4.pk')], + verbose_name='IPv4 Address' + ) + primary_ip6 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip6.pk')], + verbose_name='IPv6 Address' + ) + cluster = tables.LinkColumn( + viewname='virtualization:cluster', + args=[Accessor('cluster.pk')] + ) + virtual_chassis = tables.LinkColumn( + viewname='dcim:virtualchassis', + args=[Accessor('virtual_chassis.pk')] + ) + vc_position = tables.Column( + verbose_name='VC Position' + ) + vc_priority = tables.Column( + verbose_name='VC Priority' + ) + tags = TagColumn( + url_name='dcim:device_list' + ) class Meta(BaseTable.Meta): model = Device - fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type') - - -class DeviceDetailTable(DeviceTable): - primary_ip = tables.TemplateColumn( - orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP - ) - - class Meta(DeviceTable.Meta): - model = Device - fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') + fields = ( + 'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site', + 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'tags', + ) + default_columns = ( + 'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip', + ) class DeviceImportTable(BaseTable): - name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') - position = tables.Column(verbose_name='Position') - device_role = tables.Column(verbose_name='Role') - device_type = tables.Column(verbose_name='Type') + name = tables.TemplateColumn( + template_code=DEVICE_LINK + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + args=[Accessor('rack.pk')] + ) + device_role = tables.Column( + verbose_name='Role' + ) + device_type = tables.Column( + verbose_name='Type' + ) class Meta(BaseTable.Meta): model = Device @@ -902,23 +1060,23 @@ class CableTable(BaseTable): template_code=CABLE_TERMINATION_PARENT, accessor=Accessor('termination_a'), orderable=False, - verbose_name='Termination A' + verbose_name='Side A' ) termination_a = tables.LinkColumn( accessor=Accessor('termination_a'), orderable=False, - verbose_name='' + verbose_name='Termination A' ) termination_b_parent = tables.TemplateColumn( template_code=CABLE_TERMINATION_PARENT, accessor=Accessor('termination_b'), orderable=False, - verbose_name='Termination B' + verbose_name='Side B' ) termination_b = tables.LinkColumn( accessor=Accessor('termination_b'), orderable=False, - verbose_name='' + verbose_name='Termination B' ) status = tables.TemplateColumn( template_code=STATUS_LABEL @@ -935,6 +1093,10 @@ class CableTable(BaseTable): 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'status', 'type', 'color', 'length', ) + default_columns = ( + 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', + 'status', 'type', + ) # @@ -1002,10 +1164,6 @@ class InterfaceConnectionTable(BaseTable): args=[Accessor('pk')], verbose_name='Interface A' ) - description_a = tables.Column( - accessor=Accessor('description'), - verbose_name='Description' - ) device_b = tables.LinkColumn( viewname='dcim:device', accessor=Accessor('_connected_interface.device'), @@ -1018,15 +1176,11 @@ class InterfaceConnectionTable(BaseTable): args=[Accessor('_connected_interface.pk')], verbose_name='Interface B' ) - description_b = tables.Column( - accessor=Accessor('_connected_interface.description'), - verbose_name='Description' - ) class Meta(BaseTable.Meta): model = Interface fields = ( - 'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status', + 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status', ) @@ -1036,12 +1190,21 @@ class InterfaceConnectionTable(BaseTable): class InventoryItemTable(BaseTable): pk = ToggleColumn() - device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')]) - manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer') + device = tables.LinkColumn( + viewname='dcim:device_inventory', + args=[Accessor('device.pk')] + ) + manufacturer = tables.Column( + accessor=Accessor('manufacturer.name') + ) + discovered = BooleanColumn() class Meta(BaseTable.Meta): model = InventoryItem - fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description') + fields = ( + 'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered' + ) + default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag') # @@ -1050,17 +1213,21 @@ class InventoryItemTable(BaseTable): class VirtualChassisTable(BaseTable): pk = ToggleColumn() - master = tables.LinkColumn() - member_count = tables.Column(verbose_name='Members') - actions = tables.TemplateColumn( - template_code=VIRTUALCHASSIS_ACTIONS, - attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='' + name = tables.Column( + accessor=Accessor('master__name'), + linkify=True + ) + member_count = tables.Column( + verbose_name='Members' + ) + tags = TagColumn( + url_name='dcim:virtualchassis_list' ) class Meta(BaseTable.Meta): model = VirtualChassis - fields = ('pk', 'master', 'domain', 'member_count', 'actions') + fields = ('pk', 'name', 'domain', 'member_count', 'tags') + default_columns = ('pk', 'name', 'domain', 'member_count') # @@ -1082,6 +1249,7 @@ class PowerPanelTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPanel fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') + default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') # @@ -1105,7 +1273,22 @@ class PowerFeedTable(BaseTable): type = tables.TemplateColumn( template_code=TYPE_LABEL ) + max_utilization = tables.TemplateColumn( + template_code="{{ value }}%" + ) + available_power = tables.Column( + verbose_name='Available power (VA)' + ) + tags = TagColumn( + url_name='dcim:powerfeed_list' + ) class Meta(BaseTable.Meta): model = PowerFeed - fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase') + fields = ( + 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', + 'max_utilization', 'available_power', 'tags', + ) + default_columns = ( + 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', + ) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 3158596fc..6c261f025 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -42,8 +42,7 @@ class RegionTestCase(TestCase): region.save() def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -104,8 +103,7 @@ class SiteTestCase(TestCase): Site.objects.bulk_create(sites) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -209,8 +207,7 @@ class RackGroupTestCase(TestCase): rackgroup.save() def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -262,8 +259,7 @@ class RackRoleTestCase(TestCase): RackRole.objects.bulk_create(rack_roles) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -339,8 +335,7 @@ class RackTestCase(TestCase): Rack.objects.bulk_create(racks) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -499,6 +494,10 @@ class RackReservationTestCase(TestCase): ) RackReservation.objects.bulk_create(reservations) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_site(self): sites = Site.objects.all()[:2] params = {'site_id': [sites[0].pk, sites[1].pk]} @@ -551,8 +550,7 @@ class ManufacturerTestCase(TestCase): Manufacturer.objects.bulk_create(manufacturers) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -624,6 +622,10 @@ class DeviceTypeTestCase(TestCase): DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), )) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_model(self): params = {'model': ['Model 1', 'Model 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -723,8 +725,7 @@ class ConsolePortTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -760,8 +761,7 @@ class ConsoleServerPortTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -797,8 +797,7 @@ class PowerPortTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -842,8 +841,7 @@ class PowerOutletTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -884,8 +882,7 @@ class InterfaceTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -939,8 +936,7 @@ class FrontPortTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -981,8 +977,7 @@ class RearPortTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1027,8 +1022,7 @@ class DeviceBayTemplateTestCase(TestCase): )) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1056,8 +1050,7 @@ class DeviceRoleTestCase(TestCase): DeviceRole.objects.bulk_create(device_roles) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1101,8 +1094,7 @@ class PlatformTestCase(TestCase): Platform.objects.bulk_create(platforms) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1275,8 +1267,7 @@ class DeviceTestCase(TestCase): Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1512,8 +1503,7 @@ class ConsolePortTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1608,8 +1598,7 @@ class ConsoleServerPortTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1704,8 +1693,7 @@ class PowerPortTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1808,8 +1796,7 @@ class PowerOutletTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -1906,9 +1893,8 @@ class InterfaceTestCase(TestCase): # Third pair is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:3] - params = {'id': [str(id) for id in id_list]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): params = {'name': ['Interface 1', 'Interface 2']} @@ -2043,8 +2029,7 @@ class FrontPortTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -2136,8 +2121,7 @@ class RearPortTestCase(TestCase): # Third port is not connected def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -2224,8 +2208,7 @@ class DeviceBayTestCase(TestCase): DeviceBay.objects.bulk_create(device_bays) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -2312,8 +2295,7 @@ class InventoryItemTestCase(TestCase): InventoryItem.objects.bulk_create(child_inventory_items) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -2424,8 +2406,7 @@ class VirtualChassisTestCase(TestCase): Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2]) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_domain(self): @@ -2513,8 +2494,7 @@ class CableTestCase(TestCase): Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_label(self): @@ -2609,6 +2589,10 @@ class PowerPanelTestCase(TestCase): ) PowerPanel.objects.bulk_create(power_panels) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Power Panel 1', 'Power Panel 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2676,6 +2660,10 @@ class PowerFeedTestCase(TestCase): ) PowerFeed.objects.bulk_create(power_feeds) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Power Feed 1', 'Power Feed 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 7be9ef6e4..6db938732 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -514,10 +514,10 @@ class CablePathTestCase(TestCase): def test_direct_connection(self): """ + Test a direct connection between two interfaces. [Device 1] ----- [Device 2] Iface1 Iface1 - """ # Create cable cable = Cable( @@ -549,6 +549,49 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) + def test_connection_via_single_rear_port(self): + """ + Test a connection which passes through a single front/rear port pair. + + 1 2 + [Device 1] ----- [Panel 1] ----- [Device 2] + Iface1 FP1 RP1 Iface1 + """ + # Create cables + cable1 = Cable( + termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), + termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') + ) + cable1.save() + cable2 = Cable( + termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), + termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') + ) + cable2.save() + + # Retrieve endpoints + endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') + endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') + + # Validate connections + self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) + self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) + self.assertTrue(endpoint_a.connection_status) + self.assertTrue(endpoint_b.connection_status) + + # Delete cable 1 + cable1.delete() + + # Refresh endpoints + endpoint_a.refresh_from_db() + endpoint_b.refresh_from_db() + + # Check that connections have been nullified + self.assertIsNone(endpoint_a.connected_endpoint) + self.assertIsNone(endpoint_b.connected_endpoint) + self.assertIsNone(endpoint_a.connection_status) + self.assertIsNone(endpoint_b.connection_status) + def test_connections_via_patch(self): """ Test two connections via patched rear ports: diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 2feaf625b..65f37c1d5 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -184,7 +184,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): site = Site.objects.create(name='Site 1', slug='site-1') - rack = Rack(name='Rack 1', site=site) + rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site) + rack_group.save() + + rack = Rack(name='Rack 1', site=site, group=rack_group) rack.save() RackReservation.objects.bulk_create([ @@ -202,10 +205,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'site,rack_name,units,description', - 'Site 1,Rack 1,"10,11,12",Reservation 1', - 'Site 1,Rack 1,"13,14,15",Reservation 2', - 'Site 1,Rack 1,"16,17,18",Reservation 3', + 'site,rack_group,rack,units,description', + 'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1', + 'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2', + 'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3', ) cls.bulk_edit_data = { @@ -268,10 +271,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): } 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", + "site,group,name,width,u_height", + "Site 1,,Rack 4,19,42", + "Site 1,Rack Group 1,Rack 5,19,42", + "Site 2,Rack Group 2,Rack 6,19,42", ) cls.bulk_edit_data = { @@ -890,8 +893,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) Site.objects.bulk_create(sites) + rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1') + rack_group.save() + racks = ( - Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 1', site=sites[0], group=rack_group), Rack(name='Rack 2', site=sites[1]), ) Rack.objects.bulk_create(racks) @@ -947,10 +953,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): } 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", + "device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 4,Site 1,Rack Group 1,Rack 1,10,Front", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 5,Site 1,Rack Group 1,Rack 1,20,Front", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 6,Site 1,Rack Group 1,Rack 1,30,Front", ) cls.bulk_edit_data = { @@ -1507,10 +1513,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = VirtualChassis # 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 @@ -1589,7 +1592,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "site,rack_group_name,name", + "site,rack_group,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", @@ -1648,7 +1651,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "site,panel_name,name,voltage,amperage,max_utilization", + "site,power_panel,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", diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 36a272cf8..0b1f6250e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -321,6 +321,9 @@ urlpatterns = [ # Virtual chassis 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.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'), + path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'), + path('virtual-chassis//', views.VirtualChassisView.as_view(), name='virtualchassis'), 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}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9ca4c2edc..cd1b4edf4 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -32,7 +32,6 @@ from virtualization.models import VirtualMachine from . import filters, forms, tables from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES -from .exceptions import CableTraceSplit from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -1096,7 +1095,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView): ) filterset = filters.DeviceFilterSet filterset_form = forms.DeviceFilterForm - table = tables.DeviceDetailTable + table = tables.DeviceTable template_name = 'dcim/device_list.html' @@ -2279,19 +2278,15 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): csv_data = [ # Headers ','.join([ - 'device_a', 'interface_a', 'interface_a_description', - 'device_b', 'interface_b', 'interface_b_description', - 'connection_status' + 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status' ]) ] for obj in self.queryset: csv = csv_format([ obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj.connected_endpoint.name if obj.connected_endpoint else None, - obj.connected_endpoint.description if obj.connected_endpoint else None, obj.device.identifier, obj.name, - obj.description, obj.get_connection_status_display(), ]) csv_data.append(csv) @@ -2368,6 +2363,17 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView): action_buttons = ('export',) +class VirtualChassisView(PermissionRequiredMixin, View): + permission_required = 'dcim.view_virtualchassis' + + def get(self, request, pk): + virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk) + + return render(request, 'dcim/virtualchassis.html', { + 'virtualchassis': virtualchassis, + }) + + class VirtualChassisCreateView(PermissionRequiredMixin, View): permission_required = 'dcim.add_virtualchassis' @@ -2595,6 +2601,23 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, }) +class VirtualChassisBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_virtualchassis' + queryset = VirtualChassis.objects.all() + filterset = filters.VirtualChassisFilterSet + table = tables.VirtualChassisTable + form = forms.VirtualChassisBulkEditForm + default_return_url = 'dcim:virtualchassis_list' + + +class VirtualChassisBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_virtualchassis' + queryset = VirtualChassis.objects.all() + filterset = filters.VirtualChassisFilterSet + table = tables.VirtualChassisTable + default_return_url = 'dcim:virtualchassis_list' + + # # Power panels # diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index ad414a691..7ccdb1d86 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -94,14 +94,14 @@ class GraphFilterSet(BaseFilterSet): class Meta: model = Graph - fields = ['type', 'name', 'template_language'] + fields = ['id', 'type', 'name', 'template_language'] class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate - fields = ['content_type', 'name', 'template_language'] + fields = ['id', 'content_type', 'name', 'template_language'] class TagFilterSet(BaseFilterSet): @@ -112,7 +112,7 @@ class TagFilterSet(BaseFilterSet): class Meta: model = Tag - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug', 'color'] def search(self, queryset, name, value): if not value.strip(): @@ -219,7 +219,7 @@ class ConfigContextFilterSet(BaseFilterSet): class Meta: model = ConfigContext - fields = ['name', 'is_active'] + fields = ['id', 'name', 'is_active'] def search(self, queryset, name, value): if not value.strip(): @@ -255,7 +255,8 @@ class ObjectChangeFilterSet(BaseFilterSet): class Meta: model = ObjectChange fields = [ - 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr', + 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', + 'object_repr', ] def search(self, queryset, name, value): diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 7ec9d2285..384b3563b 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,7 +8,7 @@ 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, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, + ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup @@ -89,7 +89,7 @@ class CustomFieldModelForm(forms.ModelForm): return obj -class CustomFieldModelCSVForm(CustomFieldModelForm): +class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): def _append_customfield_fields(self): @@ -229,7 +229,6 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), - to_field_name='slug', required=False ) data = JSONField( diff --git a/netbox/extras/migrations/0001_initial_squashed_0013_objectchange.py b/netbox/extras/migrations/0001_initial_squashed_0013_objectchange.py deleted file mode 100644 index 112a8c9af..000000000 --- a/netbox/extras/migrations/0001_initial_squashed_0013_objectchange.py +++ /dev/null @@ -1,265 +0,0 @@ -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.conf import settings -from django.db import connection, migrations, models -from django.db.utils import OperationalError - -import extras.models - - -def verify_postgresql_version(apps, schema_editor): - """ - Verify that PostgreSQL is version 9.4 or higher. - """ - # https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION - DB_MINIMUM_VERSION = 90400 # 9.4.0 - - try: - pg_version = connection.pg_version - - if pg_version < DB_MINIMUM_VERSION: - raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version)) - - # Skip if the database is missing (e.g. for CI testing) or misconfigured. - except OperationalError: - pass - - -def is_filterable_to_filter_logic(apps, schema_editor): - CustomField = apps.get_model('extras', 'CustomField') - CustomField.objects.filter(is_filterable=False).update(filter_logic=0) - CustomField.objects.filter(is_filterable=True).update(filter_logic=1) - # Select fields match on primary key only - CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2) - - -class Migration(migrations.Migration): - - replaces = [('extras', '0001_initial'), ('extras', '0002_custom_fields'), ('extras', '0003_exporttemplate_add_description'), ('extras', '0004_topologymap_change_comma_to_semicolon'), ('extras', '0005_useraction_add_bulk_create'), ('extras', '0006_add_imageattachments'), ('extras', '0007_unicode_literals'), ('extras', '0008_reports'), ('extras', '0009_topologymap_type'), ('extras', '0010_customfield_filter_logic'), ('extras', '0011_django2'), ('extras', '0012_webhooks'), ('extras', '0013_objectchange')] - - dependencies = [ - ('dcim', '0002_auto_20160622_1821'), - ('contenttypes', '0002_remove_content_type_name'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CustomField', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100)), - ('name', models.CharField(max_length=50, unique=True)), - ('label', models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)), - ('description', models.CharField(blank=True, max_length=100)), - ('required', models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.')), - ('is_filterable', models.BooleanField(default=True, help_text='This field can be used to filter objects.')), - ('default', models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100)), - ('weight', models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form')), - ('obj_type', models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)')), - ], - options={ - 'ordering': ['weight', 'name'], - }, - ), - migrations.CreateModel( - name='CustomFieldValue', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('obj_id', models.PositiveIntegerField()), - ('serialized_value', models.CharField(max_length=255)), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')), - ('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')), - ], - options={ - 'ordering': ['obj_type', 'obj_id'], - 'unique_together': {('field', 'obj_type', 'obj_id')}, - }, - ), - migrations.CreateModel( - name='ExportTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('template_code', models.TextField()), - ('mime_type', models.CharField(blank=True, max_length=15)), - ('file_extension', models.CharField(blank=True, max_length=15)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('description', models.CharField(blank=True, max_length=200)), - ], - options={ - 'ordering': ['content_type', 'name'], - 'unique_together': {('content_type', 'name')}, - }, - ), - migrations.CreateModel( - name='CustomFieldChoice', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.CharField(max_length=100)), - ('weight', models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list')), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')), - ], - options={ - 'ordering': ['field', 'weight', 'value'], - 'unique_together': {('field', 'value')}, - }, - ), - migrations.CreateModel( - name='Graph', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')])), - ('weight', models.PositiveSmallIntegerField(default=1000)), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('source', models.CharField(max_length=500, verbose_name='Source URL')), - ('link', models.URLField(blank=True, verbose_name='Link URL')), - ], - options={ - 'ordering': ['type', 'weight', 'name'], - }, - ), - migrations.CreateModel( - name='ImageAttachment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('image', models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width')), - ('image_height', models.PositiveSmallIntegerField()), - ('image_width', models.PositiveSmallIntegerField()), - ('name', models.CharField(blank=True, max_length=50)), - ('created', models.DateTimeField(auto_now_add=True)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='TopologyMap', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('device_patterns', models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.')), - ('description', models.CharField(blank=True, max_length=100)), - ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='UserAction', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True)), - ('object_id', models.PositiveIntegerField(blank=True, null=True)), - ('action', models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')])), - ('message', models.TextField(blank=True)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-time'], - }, - ), - migrations.RunPython( - code=verify_postgresql_version, - ), - migrations.CreateModel( - name='ReportResult', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('report', models.CharField(max_length=255, unique=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('failed', models.BooleanField()), - ('data', django.contrib.postgres.fields.jsonb.JSONField()), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['report'], - }, - ), - migrations.AddField( - model_name='topologymap', - name='type', - field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1), - ), - migrations.AddField( - model_name='customfield', - name='filter_logic', - field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'), - ), - migrations.AlterField( - model_name='customfield', - name='required', - field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'), - ), - migrations.AlterField( - model_name='customfield', - name='weight', - field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'), - ), - migrations.RunPython( - code=is_filterable_to_filter_logic, - ), - migrations.RemoveField( - model_name='customfield', - name='is_filterable', - ), - migrations.AlterField( - model_name='customfield', - name='obj_type', - field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'), - ), - migrations.AlterField( - model_name='customfieldchoice', - name='field', - field=models.ForeignKey(limit_choices_to={'type': 600}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'), - ), - migrations.AlterField( - model_name='exporttemplate', - name='content_type', - field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.CreateModel( - name='Webhook', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=150, unique=True)), - ('type_create', models.BooleanField(default=False, help_text='Call this webhook when a matching object is created.')), - ('type_update', models.BooleanField(default=False, help_text='Call this webhook when a matching object is updated.')), - ('type_delete', models.BooleanField(default=False, help_text='Call this webhook when a matching object is deleted.')), - ('payload_url', models.CharField(help_text='A POST will be sent to this URL when the webhook is called.', max_length=500, verbose_name='URL')), - ('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1, verbose_name='HTTP content type')), - ('secret', models.CharField(blank=True, help_text="When provided, the request will include a 'X-Hook-Signature' header containing a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)), - ('enabled', models.BooleanField(default=True)), - ('ssl_verification', models.BooleanField(default=True, help_text='Enable SSL certificate verification. Disable with caution!', verbose_name='SSL verification')), - ('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types')), - ], - options={ - 'unique_together': {('payload_url', 'type_create', 'type_update', 'type_delete')}, - }, - ), - migrations.CreateModel( - name='ObjectChange', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True)), - ('user_name', models.CharField(editable=False, max_length=150)), - ('request_id', models.UUIDField(editable=False)), - ('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])), - ('changed_object_id', models.PositiveIntegerField()), - ('related_object_id', models.PositiveIntegerField(blank=True, null=True)), - ('object_repr', models.CharField(editable=False, max_length=200)), - ('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)), - ('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')), - ('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-time'], - }, - ), - ] diff --git a/netbox/extras/migrations/0014_configcontexts_squashed_0019_tag_taggeditem.py b/netbox/extras/migrations/0014_configcontexts_squashed_0019_tag_taggeditem.py deleted file mode 100644 index 6f8b63649..000000000 --- a/netbox/extras/migrations/0014_configcontexts_squashed_0019_tag_taggeditem.py +++ /dev/null @@ -1,106 +0,0 @@ -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.db import migrations, models - - -def set_template_language(apps, schema_editor): - """ - Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates). - """ - ExportTemplate = apps.get_model('extras', 'ExportTemplate') - ExportTemplate.objects.update(template_language=10) - - -class Migration(migrations.Migration): - - replaces = [('extras', '0014_configcontexts'), ('extras', '0015_remove_useraction'), ('extras', '0016_exporttemplate_add_cable'), ('extras', '0017_exporttemplate_mime_type_length'), ('extras', '0018_exporttemplate_add_jinja2'), ('extras', '0019_tag_taggeditem')] - - dependencies = [ - ('extras', '0013_objectchange'), - ('tenancy', '0005_change_logging'), - ('dcim', '0061_platform_napalm_args'), - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='ConfigContext', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('weight', models.PositiveSmallIntegerField(default=1000)), - ('description', models.CharField(blank=True, max_length=100)), - ('is_active', models.BooleanField(default=True)), - ('data', django.contrib.postgres.fields.jsonb.JSONField()), - ('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')), - ('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')), - ('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')), - ('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')), - ('tenant_groups', models.ManyToManyField(blank=True, related_name='_configcontext_tenant_groups_+', to='tenancy.TenantGroup')), - ('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')), - ], - options={ - 'ordering': ['weight', 'name'], - }, - ), - migrations.AlterField( - model_name='customfield', - name='obj_type', - field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'), - ), - migrations.AlterField( - model_name='exporttemplate', - name='content_type', - field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.AlterField( - model_name='webhook', - name='obj_type', - field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'virtualchassis', 'consoleport', 'consoleserverport', 'powerport', 'poweroutlet', 'interface', 'devicebay', 'inventoryitem', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'), - ), - migrations.DeleteModel( - name='UserAction', - ), - migrations.AlterField( - model_name='exporttemplate', - name='content_type', - field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.AlterField( - model_name='exporttemplate', - name='mime_type', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='exporttemplate', - name='template_language', - field=models.PositiveSmallIntegerField(default=20), - ), - migrations.RunPython( - code=set_template_language, - ), - migrations.CreateModel( - name='Tag', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(max_length=100, unique=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='TaggedItem', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('object_id', models.IntegerField(db_index=True)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')), - ], - options={ - 'abstract': False, - 'index_together': {('content_type', 'object_id')}, - }, - ), - ] diff --git a/netbox/extras/migrations/0020_tag_data_squashed_0021_add_color_comments_changelog_to_tag.py b/netbox/extras/migrations/0020_tag_data_squashed_0021_add_color_comments_changelog_to_tag.py deleted file mode 100644 index a673bf880..000000000 --- a/netbox/extras/migrations/0020_tag_data_squashed_0021_add_color_comments_changelog_to_tag.py +++ /dev/null @@ -1,93 +0,0 @@ -from django.db import migrations, models - -import utilities.fields - - -def copy_tags(apps, schema_editor): - """ - Copy data from taggit_tag to extras_tag - """ - TaggitTag = apps.get_model('taggit', 'Tag') - ExtrasTag = apps.get_model('extras', 'Tag') - - tags_values = TaggitTag.objects.all().values('id', 'name', 'slug') - tags = [ExtrasTag(**tag) for tag in tags_values] - ExtrasTag.objects.bulk_create(tags) - - -def copy_taggeditems(apps, schema_editor): - """ - Copy data from taggit_taggeditem to extras_taggeditem - """ - TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem') - ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem') - - tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id') - tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values] - ExtrasTaggedItem.objects.bulk_create(tagged_items) - - -def delete_taggit_taggeditems(apps, schema_editor): - """ - Delete all TaggedItem instances from taggit_taggeditem - """ - TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem') - TaggitTaggedItem.objects.all().delete() - - -def delete_taggit_tags(apps, schema_editor): - """ - Delete all Tag instances from taggit_tag - """ - TaggitTag = apps.get_model('taggit', 'Tag') - TaggitTag.objects.all().delete() - - -class Migration(migrations.Migration): - - replaces = [('extras', '0020_tag_data'), ('extras', '0021_add_color_comments_changelog_to_tag')] - - dependencies = [ - ('extras', '0019_tag_taggeditem'), - ('virtualization', '0009_custom_tag_models'), - ('tenancy', '0006_custom_tag_models'), - ('secrets', '0006_custom_tag_models'), - ('dcim', '0070_custom_tag_models'), - ('ipam', '0025_custom_tag_models'), - ('circuits', '0015_custom_tag_models'), - ] - - operations = [ - migrations.RunPython( - code=copy_tags, - ), - migrations.RunPython( - code=copy_taggeditems, - ), - migrations.RunPython( - code=delete_taggit_taggeditems, - ), - migrations.RunPython( - code=delete_taggit_tags, - ), - migrations.AddField( - model_name='tag', - name='color', - field=utilities.fields.ColorField(default='9e9e9e', max_length=6), - ), - migrations.AddField( - model_name='tag', - name='comments', - field=models.TextField(blank=True, default=''), - ), - migrations.AddField( - model_name='tag', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='tag', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0022_custom_links_squashed_0034_configcontext_tags.py b/netbox/extras/migrations/0022_custom_links_squashed_0034_configcontext_tags.py deleted file mode 100644 index b10841a6a..000000000 --- a/netbox/extras/migrations/0022_custom_links_squashed_0034_configcontext_tags.py +++ /dev/null @@ -1,227 +0,0 @@ -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.db import migrations, models - -import extras.models - -CUSTOMFIELD_TYPE_CHOICES = ( - (100, 'text'), - (200, 'integer'), - (300, 'boolean'), - (400, 'date'), - (500, 'url'), - (600, 'select') -) - -CUSTOMFIELD_FILTER_LOGIC_CHOICES = ( - (0, 'disabled'), - (1, 'integer'), - (2, 'exact'), -) - -OBJECTCHANGE_ACTION_CHOICES = ( - (1, 'create'), - (2, 'update'), - (3, 'delete'), -) - -EXPORTTEMPLATE_LANGUAGE_CHOICES = ( - (10, 'django'), - (20, 'jinja2'), -) - -WEBHOOK_CONTENTTYPE_CHOICES = ( - (1, 'application/json'), - (2, 'application/x-www-form-urlencoded'), -) - -GRAPH_TYPE_CHOICES = ( - (100, 'dcim', 'interface'), - (150, 'dcim', 'device'), - (200, 'circuits', 'provider'), - (300, 'dcim', 'site'), -) - - -def customfield_type_to_slug(apps, schema_editor): - CustomField = apps.get_model('extras', 'CustomField') - for id, slug in CUSTOMFIELD_TYPE_CHOICES: - CustomField.objects.filter(type=str(id)).update(type=slug) - - -def customfield_filter_logic_to_slug(apps, schema_editor): - CustomField = apps.get_model('extras', 'CustomField') - for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES: - CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug) - - -def objectchange_action_to_slug(apps, schema_editor): - ObjectChange = apps.get_model('extras', 'ObjectChange') - for id, slug in OBJECTCHANGE_ACTION_CHOICES: - ObjectChange.objects.filter(action=str(id)).update(action=slug) - - -def exporttemplate_language_to_slug(apps, schema_editor): - ExportTemplate = apps.get_model('extras', 'ExportTemplate') - for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES: - ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug) - - -def webhook_contenttype_to_slug(apps, schema_editor): - Webhook = apps.get_model('extras', 'Webhook') - for id, slug in WEBHOOK_CONTENTTYPE_CHOICES: - Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug) - - -def graph_type_to_fk(apps, schema_editor): - Graph = apps.get_model('extras', 'Graph') - ContentType = apps.get_model('contenttypes', 'ContentType') - - # On a new installation (and during tests) content types might not yet exist. So, we only perform the bulk - # updates if a Graph has been created, which implies that we're working with a populated database. - if Graph.objects.exists(): - for id, app_label, model in GRAPH_TYPE_CHOICES: - content_type = ContentType.objects.get(app_label=app_label, model=model) - Graph.objects.filter(type=id).update(type=content_type.pk) - - -class Migration(migrations.Migration): - - replaces = [('extras', '0022_custom_links'), ('extras', '0023_fix_tag_sequences'), ('extras', '0024_scripts'), ('extras', '0025_objectchange_time_index'), ('extras', '0026_webhook_ca_file_path'), ('extras', '0027_webhook_additional_headers'), ('extras', '0028_remove_topology_maps'), ('extras', '0029_3569_customfield_fields'), ('extras', '0030_3569_objectchange_fields'), ('extras', '0031_3569_exporttemplate_fields'), ('extras', '0032_3569_webhook_fields'), ('extras', '0033_graph_type_template_language'), ('extras', '0034_configcontext_tags')] - - dependencies = [ - ('extras', '0021_add_color_comments_changelog_to_tag'), - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='CustomLink', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('text', models.CharField(max_length=500)), - ('url', models.CharField(max_length=500)), - ('weight', models.PositiveSmallIntegerField(default=100)), - ('group_name', models.CharField(blank=True, max_length=50)), - ('button_class', models.CharField(default='default', max_length=30)), - ('new_window', models.BooleanField()), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ], - options={ - 'ordering': ['group_name', 'weight', 'name'], - }, - ), - migrations.AlterField( - model_name='customfield', - name='obj_type', - field=models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType'), - ), - migrations.AlterField( - model_name='exporttemplate', - name='content_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.AlterField( - model_name='webhook', - name='obj_type', - field=models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType'), - ), - migrations.RunSQL( - sql="SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)", - ), - migrations.RunSQL( - sql="SELECT setval('extras_taggeditem_id_seq', (SELECT id FROM extras_taggeditem ORDER BY id DESC LIMIT 1) + 1)", - ), - migrations.CreateModel( - name='Script', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ], - options={ - 'permissions': (('run_script', 'Can run script'),), - 'managed': False, - }, - ), - migrations.AlterField( - model_name='objectchange', - name='time', - field=models.DateTimeField(auto_now_add=True, db_index=True), - ), - migrations.AddField( - model_name='webhook', - name='ca_file_path', - field=models.CharField(blank=True, max_length=4096, null=True), - ), - migrations.AddField( - model_name='webhook', - name='additional_headers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), - ), - migrations.DeleteModel( - name='TopologyMap', - ), - migrations.AlterField( - model_name='customfield', - name='type', - field=models.CharField(default='text', max_length=50), - ), - migrations.RunPython( - code=customfield_type_to_slug, - ), - migrations.AlterField( - model_name='customfieldchoice', - name='field', - field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'), - ), - migrations.AlterField( - model_name='customfield', - name='filter_logic', - field=models.CharField(default='loose', max_length=50), - ), - migrations.RunPython( - code=customfield_filter_logic_to_slug, - ), - migrations.AlterField( - model_name='objectchange', - name='action', - field=models.CharField(max_length=50), - ), - migrations.RunPython( - code=objectchange_action_to_slug, - ), - migrations.AlterField( - model_name='exporttemplate', - name='template_language', - field=models.CharField(default='jinja2', max_length=50), - ), - migrations.RunPython( - code=exporttemplate_language_to_slug, - ), - migrations.AlterField( - model_name='webhook', - name='http_content_type', - field=models.CharField(default='application/json', max_length=50), - ), - migrations.RunPython( - code=webhook_contenttype_to_slug, - ), - migrations.RunPython( - code=graph_type_to_fk, - ), - migrations.AlterField( - model_name='graph', - name='type', - field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'device', 'interface', 'site']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.AddField( - model_name='graph', - name='template_language', - field=models.CharField(default='jinja2', max_length=50), - ), - migrations.AddField( - model_name='configcontext', - name='tags', - field=models.ManyToManyField(blank=True, related_name='_configcontext_tags_+', to='extras.Tag'), - ), - ] diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index b145824c6..7a78d4b19 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -104,7 +104,11 @@ class ConfigContextTable(BaseTable): class Meta(BaseTable.Meta): model = ConfigContext - fields = ('pk', 'name', 'weight', 'is_active', 'description') + fields = ( + 'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', + ) + default_columns = ('pk', 'name', 'weight', 'is_active', 'description') class ObjectChangeTable(BaseTable): diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 25a839c39..72db138e2 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -5,7 +5,7 @@ from dcim.models import DeviceRole, Platform, Region, Site from extras.choices import * from extras.filters import * from extras.utils import FeatureQuery -from extras.models import ConfigContext, ExportTemplate, Graph +from extras.models import ConfigContext, ExportTemplate, Graph, Tag from tenancy.models import Tenant, TenantGroup from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -27,6 +27,10 @@ class GraphTestCase(TestCase): ) Graph.objects.bulk_create(graphs) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Graph 1', 'Graph 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -57,6 +61,10 @@ class ExportTemplateTestCase(TestCase): ) ExportTemplate.objects.bulk_create(export_templates) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Export Template 1', 'Export Template 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -153,6 +161,10 @@ class ConfigContextTestCase(TestCase): c.tenant_groups.set([tenant_groups[i]]) c.tenants.set([tenants[i]]) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['Config Context 1', 'Config Context 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -218,4 +230,35 @@ class ConfigContextTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) +class TagTestCase(TestCase): + queryset = Tag.objects.all() + filterset = TagFilterSet + + @classmethod + def setUpTestData(cls): + + tags = ( + Tag(name='Tag 1', slug='tag-1', color='ff0000'), + Tag(name='Tag 2', slug='tag-2', color='00ff00'), + Tag(name='Tag 3', slug='tag-3', color='0000ff'), + ) + Tag.objects.bulk_create(tags) + + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Tag 1', 'Tag 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['tag-1', 'tag-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_color(self): + params = {'color': ['ff0000', '00ff00']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # TODO: ObjectChangeFilter test diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_webhooks.py index 06b4f7c7e..f87242404 100644 --- a/netbox/extras/tests/test_webhooks.py +++ b/netbox/extras/tests/test_webhooks.py @@ -102,7 +102,7 @@ class WebhookTest(APITestCase): request_id = uuid.uuid4() - def dummy_send(_, request): + def dummy_send(_, request, **kwargs): """ A dummy implementation of Session.send() to be used for testing. Always returns a 200 HTTP response. diff --git a/netbox/extras/views.py b/netbox/extras/views.py index bb7d76dd0..613e45132 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -119,11 +119,18 @@ class ConfigContextView(PermissionRequiredMixin, View): permission_required = 'extras.view_configcontext' def get(self, request, pk): - configcontext = get_object_or_404(ConfigContext, pk=pk) + # Determine user's preferred output format + if request.GET.get('format') in ['json', 'yaml']: + format = request.GET.get('format') + request.user.config.set('extras.configcontext.format', format, commit=True) + else: + format = request.user.config.get('extras.configcontext.format', 'json') + return render(request, 'extras/configcontext.html', { 'configcontext': configcontext, + 'format': format, }) @@ -171,11 +178,19 @@ class ObjectConfigContextView(View): source_contexts = ConfigContext.objects.get_for_object(obj) model_name = self.object_class._meta.model_name + # Determine user's preferred output format + if request.GET.get('format') in ['json', 'yaml']: + format = request.GET.get('format') + request.user.config.set('extras.configcontext.format', format, commit=True) + else: + format = request.user.config.get('extras.configcontext.format', 'json') + return render(request, 'extras/object_configcontext.html', { model_name: obj, 'obj': obj, 'rendered_context': obj.get_config_context(), 'source_contexts': source_contexts, + 'format': format, 'base_template': self.base_template, 'active_tab': 'config-context', }) diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index d1d5a59ab..8a7a07560 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -3,11 +3,11 @@ import hmac from django.contrib.contenttypes.models import ContentType from django.utils import timezone +from django_rq import get_queue from extras.models import Webhook from utilities.api import get_serializer_for_model from .choices import * -from .constants import * from .utils import FeatureQuery @@ -17,7 +17,7 @@ def generate_signature(request_body, secret): """ hmac_prep = hmac.new( key=secret.encode('utf8'), - msg=request_body.encode('utf8'), + msg=request_body, digestmod=hashlib.sha512 ) return hmac_prep.hexdigest() @@ -50,12 +50,8 @@ def enqueue_webhooks(instance, user, request_id, action): } serializer = serializer_class(instance, context=serializer_context) - # We must only import django_rq if the Webhooks feature is enabled. - # Only if we have gotten to ths point, is the feature enabled - from django_rq import get_queue + # Enqueue the webhooks webhook_queue = get_queue('default') - - # enqueue the webhooks: for webhook in webhooks: webhook_queue.enqueue( "extras.webhooks_worker.process_webhook", diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index 1b1b76dd9..e47478f9f 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -1,6 +1,7 @@ import logging import requests +from django.conf import settings from django_rq import job from jinja2.exceptions import TemplateError @@ -46,7 +47,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque 'method': webhook.http_method, 'url': webhook.payload_url, 'headers': headers, - 'data': body, + 'data': body.encode('utf8'), } logger.info( "Sending {} request to {} ({} {})".format( @@ -69,7 +70,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque session.verify = webhook.ssl_verification if webhook.ca_file_path: session.verify = webhook.ca_file_path - response = session.send(prepared_request) + response = session.send(prepared_request, proxies=settings.HTTP_PROXIES) if 200 <= response.status_code <= 299: logger.info("Request succeeded; response status {}".format(response.status_code)) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 1390da945..6641d1c8b 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -47,14 +47,14 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Create class Meta: model = VRF - fields = ['name', 'rd', 'enforce_unique'] + fields = ['id', 'name', 'rd', 'enforce_unique'] class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = RIR - fields = ['name', 'slug', 'is_private', 'description'] + fields = ['id', 'name', 'slug', 'is_private', 'description'] class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): @@ -84,7 +84,7 @@ class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt class Meta: model = Aggregate - fields = ('date_added',) + fields = ['id', 'date_added'] def search(self, queryset, name, value): if not value.strip(): @@ -206,7 +206,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre class Meta: model = Prefix - fields = ('is_pool',) + fields = ['id', 'is_pool'] def search(self, queryset, name, value): if not value.strip(): @@ -345,7 +345,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, class Meta: model = IPAddress - fields = ('dns_name',) + fields = ['id', 'dns_name'] def search(self, queryset, name, value): if not value.strip(): @@ -478,7 +478,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat class Meta: model = VLAN - fields = ['vid', 'name'] + fields = ['id', 'vid', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 854843f2e..4e5a413dc 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.core.exceptions import MultipleObjectsReturned from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField @@ -11,16 +10,15 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, - DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, - FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + CSVModelChoiceField, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine -from .constants import * from .choices import * +from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF - PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([ (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1) ]) @@ -53,22 +51,16 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class VRFCSVForm(CustomFieldModelCSVForm): - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: model = VRF fields = VRF.csv_headers - help_texts = { - 'name': 'VRF name', - } class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -120,7 +112,7 @@ class RIRForm(BootstrapMixin, forms.ModelForm): ] -class RIRCSVForm(forms.ModelForm): +class RIRCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -168,13 +160,10 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm): class AggregateCSVForm(CustomFieldModelCSVForm): - rir = forms.ModelChoiceField( + rir = CSVModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', - help_text='Name of parent RIR', - error_messages={ - 'invalid_choice': 'RIR not found.', - } + help_text='Assigned RIR' ) class Meta: @@ -247,15 +236,12 @@ class RoleForm(BootstrapMixin, forms.ModelForm): ] -class RoleCSVForm(forms.ModelForm): +class RoleCSVForm(CSVModelForm): slug = SlugField() class Meta: model = Role fields = Role.csv_headers - help_texts = { - 'name': 'Role name', - } # @@ -333,92 +319,62 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class PrefixCSVForm(CustomFieldModelCSVForm): - vrf = FlexibleModelChoiceField( + vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Name of parent VRF (or {ID})', - error_messages={ - 'invalid_choice': 'VRF not found.', - } + help_text='Assigned VRF' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) - vlan_group = forms.CharField( - help_text='Group name of assigned VLAN', - required=False + vlan_group = CSVModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text="VLAN's group (if any)" ) - vlan_vid = forms.IntegerField( - help_text='Numeric ID of assigned VLAN', - required=False + vlan = CSVModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + to_field_name='vid', + help_text="Assigned VLAN" ) status = CSVChoiceField( choices=PrefixStatusChoices, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=Role.objects.all(), required=False, to_field_name='name', - help_text='Functional role', - error_messages={ - 'invalid_choice': 'Invalid role.', - } + help_text='Functional role' ) class Meta: model = Prefix fields = Prefix.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - vlan_group = self.cleaned_data.get('vlan_group') - vlan_vid = self.cleaned_data.get('vlan_vid') - - # Validate VLAN - if vlan_group and vlan_vid: - try: - self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid) - except VLAN.DoesNotExist: - if site: - raise forms.ValidationError("VLAN {} not found in site {} group {}".format( - vlan_vid, site, vlan_group - )) - else: - raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group)) - except MultipleObjectsReturned: - raise forms.ValidationError( - "Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group) - ) - elif vlan_vid: - try: - self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid) - except VLAN.DoesNotExist: - if site: - raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site)) - else: - raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid)) - except MultipleObjectsReturned: - raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid)) + # Limit vlan queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'), + } + self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -737,23 +693,17 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class IPAddressCSVForm(CustomFieldModelCSVForm): - vrf = FlexibleModelChoiceField( + vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Name of parent VRF (or {ID})', - error_messages={ - 'invalid_choice': 'VRF not found.', - } + help_text='Assigned VRF' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Name of the assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) status = CSVChoiceField( choices=IPAddressStatusChoices, @@ -764,27 +714,23 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): required=False, help_text='Functional role' ) - device = FlexibleModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of assigned device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + help_text='Parent device of assigned interface (if any)' ) - virtual_machine = forms.ModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned virtual machine', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + help_text='Parent VM of assigned interface (if any)' ) - interface_name = forms.CharField( - help_text='Name of assigned interface', - required=False + interface = CSVModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned interface' ) is_primary = forms.BooleanField( help_text='Make this the primary IP for the assigned device', @@ -795,38 +741,34 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): model = IPAddress fields = IPAddress.csv_headers + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit interface queryset by assigned device or virtual machine + if data.get('device'): + params = { + f"device__{self.fields['device'].to_field_name}": data.get('device') + } + elif data.get('virtual_machine'): + params = { + f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine') + } + else: + params = { + 'device': None, + 'virtual_machine': None, + } + self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params) + def clean(self): super().clean() device = self.cleaned_data.get('device') virtual_machine = self.cleaned_data.get('virtual_machine') - interface_name = self.cleaned_data.get('interface_name') is_primary = self.cleaned_data.get('is_primary') - # Validate interface - if interface_name and device: - try: - self.instance.interface = Interface.objects.get(device=device, name=interface_name) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface {} for device {}".format( - interface_name, device - )) - elif interface_name and virtual_machine: - try: - self.instance.interface = Interface.objects.get(virtual_machine=virtual_machine, name=interface_name) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface {} for virtual machine {}".format( - interface_name, virtual_machine - )) - elif interface_name: - raise forms.ValidationError("Interface given ({}) but parent device/virtual machine not specified".format( - interface_name - )) - elif device: - raise forms.ValidationError("Device specified ({}) but interface missing".format(device)) - elif virtual_machine: - raise forms.ValidationError("Virtual machine specified ({}) but interface missing".format(virtual_machine)) - # Validate is_primary if is_primary and not device and not virtual_machine: raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") @@ -993,24 +935,18 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): ] -class VLANGroupCSVForm(forms.ModelForm): - site = forms.ModelChoiceField( +class VLANGroupCSVForm(CSVModelForm): + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) slug = SlugField() class Meta: model = VLANGroup fields = VLANGroup.csv_headers - help_texts = { - 'name': 'Name of VLAN group', - } class VLANGroupFilterForm(BootstrapMixin, forms.Form): @@ -1082,40 +1018,33 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class VLANCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) - group_name = forms.CharField( - help_text='Name of VLAN group', - required=False + group = CSVModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned VLAN group' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) status = CSVChoiceField( choices=VLANStatusChoices, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=Role.objects.all(), required=False, to_field_name='name', - help_text='Functional role', - error_messages={ - 'invalid_choice': 'Invalid role.', - } + help_text='Functional role' ) class Meta: @@ -1126,25 +1055,14 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'name': 'VLAN name', } - def clean(self): - super().clean() + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - site = self.cleaned_data.get('site') - group_name = self.cleaned_data.get('group_name') + if data: - # Validate VLAN group - if group_name: - try: - self.instance.group = VLANGroup.objects.get(site=site, name=group_name) - except VLANGroup.DoesNotExist: - if site: - raise forms.ValidationError( - "VLAN group {} not found for site {}".format(group_name, site) - ) - else: - raise forms.ValidationError( - "Global VLAN group {} not found".format(group_name) - ) + # Limit vlan queryset by assigned group + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['group'].queryset = self.fields['group'].queryset.filter(**params) class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -1299,23 +1217,17 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): class ServiceCSVForm(CustomFieldModelCSVForm): - device = FlexibleModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + help_text='Required if not assigned to a VM' ) - virtual_machine = FlexibleModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of virtual machine', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + help_text='Required if not assigned to a device' ) protocol = CSVChoiceField( choices=ServiceProtocolChoices, @@ -1325,11 +1237,9 @@ class ServiceCSVForm(CustomFieldModelCSVForm): class Meta: model = Service fields = Service.csv_headers - help_texts = { - } -class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Service.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/ipam/migrations/0003_ipam_add_vlangroups_squashed_0011_rir_add_is_private.py b/netbox/ipam/migrations/0003_ipam_add_vlangroups_squashed_0011_rir_add_is_private.py deleted file mode 100644 index c3b86135c..000000000 --- a/netbox/ipam/migrations/0003_ipam_add_vlangroups_squashed_0011_rir_add_is_private.py +++ /dev/null @@ -1,100 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - -import ipam.fields - - -class Migration(migrations.Migration): - - replaces = [('ipam', '0003_ipam_add_vlangroups'), ('ipam', '0004_ipam_vlangroup_uniqueness'), ('ipam', '0005_auto_20160725_1842'), ('ipam', '0006_vrf_vlan_add_tenant'), ('ipam', '0007_prefix_ipaddress_add_tenant'), ('ipam', '0008_prefix_change_order'), ('ipam', '0009_ipaddress_add_status'), ('ipam', '0010_ipaddress_help_texts'), ('ipam', '0011_rir_add_is_private')] - - dependencies = [ - ('tenancy', '0001_initial'), - ('dcim', '0010_devicebay_installed_device_set_null'), - ('ipam', '0002_vrf_add_enforce_unique'), - ] - - operations = [ - migrations.CreateModel( - name='VLANGroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50)), - ('slug', models.SlugField()), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vlan_groups', to='dcim.Site')), - ], - options={ - 'ordering': ['site', 'name'], - 'unique_together': {('site', 'name'), ('site', 'slug')}, - 'verbose_name': 'VLAN group', - 'verbose_name_plural': 'VLAN groups', - }, - ), - migrations.AddField( - model_name='vlan', - name='group', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.VLANGroup'), - ), - migrations.AlterModelOptions( - name='vlan', - options={'ordering': ['site', 'group', 'vid'], 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'}, - ), - migrations.AlterUniqueTogether( - name='vlan', - unique_together={('group', 'vid'), ('group', 'name')}, - ), - migrations.AddField( - model_name='vlan', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AlterField( - model_name='vlan', - name='name', - field=models.CharField(max_length=64), - ), - migrations.AddField( - model_name='vlan', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='vrf', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='ipaddress', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='prefix', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'), - ), - migrations.AlterModelOptions( - name='prefix', - options={'ordering': ['vrf', 'family', 'prefix'], 'verbose_name_plural': 'prefixes'}, - ), - migrations.AddField( - model_name='ipaddress', - name='status', - field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (5, b'DHCP')], default=1, verbose_name=b'Status'), - ), - migrations.AlterField( - model_name='ipaddress', - name='address', - field=ipam.fields.IPAddressField(help_text=b'IPv4 or IPv6 address (with mask)'), - ), - migrations.AlterField( - model_name='ipaddress', - name='nat_inside', - field=models.OneToOneField(blank=True, help_text=b'The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name=b'NAT (Inside)'), - ), - migrations.AddField( - model_name='rir', - name='is_private', - field=models.BooleanField(default=False, help_text=b'IP space managed by this RIR is considered private', verbose_name=b'Private'), - ), - ] diff --git a/netbox/ipam/migrations/0012_services_squashed_0018_remove_service_uniqueness_constraint.py b/netbox/ipam/migrations/0012_services_squashed_0018_remove_service_uniqueness_constraint.py deleted file mode 100644 index 6bc5020d4..000000000 --- a/netbox/ipam/migrations/0012_services_squashed_0018_remove_service_uniqueness_constraint.py +++ /dev/null @@ -1,171 +0,0 @@ -import django.core.validators -import django.db.models.deletion -from django.db import migrations, models - -import ipam.fields - - -class Migration(migrations.Migration): - - replaces = [('ipam', '0012_services'), ('ipam', '0013_prefix_add_is_pool'), ('ipam', '0014_ipaddress_status_add_deprecated'), ('ipam', '0015_global_vlans'), ('ipam', '0016_unicode_literals'), ('ipam', '0017_ipaddress_roles'), ('ipam', '0018_remove_service_uniqueness_constraint')] - - dependencies = [ - ('dcim', '0022_color_names_to_rgb'), - ('ipam', '0011_rir_add_is_private'), - ] - - operations = [ - migrations.AlterField( - model_name='prefix', - name='prefix', - field=ipam.fields.IPNetworkField(help_text=b'IPv4 or IPv6 network with mask'), - ), - migrations.AlterField( - model_name='prefix', - name='role', - field=models.ForeignKey(blank=True, help_text=b'The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'), - ), - migrations.AlterField( - model_name='prefix', - name='status', - field=models.PositiveSmallIntegerField(choices=[(0, b'Container'), (1, b'Active'), (2, b'Reserved'), (3, b'Deprecated')], default=1, help_text=b'Operational status of this prefix', verbose_name=b'Status'), - ), - migrations.AlterField( - model_name='ipaddress', - name='status', - field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (3, b'Deprecated'), (5, b'DHCP')], default=1, verbose_name=b'Status'), - ), - migrations.AlterField( - model_name='vlan', - name='site', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'), - ), - migrations.AlterField( - model_name='vlangroup', - name='site', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'), - ), - migrations.AlterField( - model_name='aggregate', - name='family', - field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]), - ), - migrations.AlterField( - model_name='aggregate', - name='rir', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'), - ), - migrations.AlterField( - model_name='ipaddress', - name='address', - field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'), - ), - migrations.AlterField( - model_name='ipaddress', - name='family', - field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), - ), - migrations.AlterField( - model_name='ipaddress', - name='nat_inside', - field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'), - ), - migrations.AlterField( - model_name='ipaddress', - name='status', - field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'), - ), - migrations.AlterField( - model_name='ipaddress', - name='vrf', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'), - ), - migrations.AlterField( - model_name='prefix', - name='family', - field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), - ), - migrations.AddField( - model_name='prefix', - name='is_pool', - field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'), - ), - migrations.AlterField( - model_name='prefix', - name='prefix', - field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'), - ), - migrations.AlterField( - model_name='prefix', - name='role', - field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'), - ), - migrations.AlterField( - model_name='prefix', - name='status', - field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'), - ), - migrations.AlterField( - model_name='prefix', - name='vlan', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'), - ), - migrations.AlterField( - model_name='prefix', - name='vrf', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'), - ), - migrations.AlterField( - model_name='rir', - name='is_private', - field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'), - ), - migrations.AlterField( - model_name='vlan', - name='status', - field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'), - ), - migrations.AlterField( - model_name='vlan', - name='vid', - field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'), - ), - migrations.AlterField( - model_name='vrf', - name='enforce_unique', - field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'), - ), - migrations.AlterField( - model_name='vrf', - name='rd', - field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'), - ), - migrations.AddField( - model_name='ipaddress', - name='role', - field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'), - ), - migrations.AlterField( - model_name='ipaddress', - name='status', - field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'), - ), - migrations.CreateModel( - name='Service', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=30)), - ('protocol', models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')])), - ('port', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number')), - ('description', models.CharField(blank=True, max_length=100)), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device')), - ('ipaddresses', models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses')), - ], - options={ - 'ordering': ['device', 'protocol', 'port'], - 'unique_together': set(), - }, - ), - ] diff --git a/netbox/ipam/migrations/0019_virtualization_squashed_0020_ipaddress_add_role_carp.py b/netbox/ipam/migrations/0019_virtualization_squashed_0020_ipaddress_add_role_carp.py deleted file mode 100644 index c0b67464f..000000000 --- a/netbox/ipam/migrations/0019_virtualization_squashed_0020_ipaddress_add_role_carp.py +++ /dev/null @@ -1,34 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - replaces = [('ipam', '0019_virtualization'), ('ipam', '0020_ipaddress_add_role_carp')] - - dependencies = [ - ('ipam', '0018_remove_service_uniqueness_constraint'), - ('virtualization', '0001_virtualization'), - ] - - operations = [ - migrations.AlterModelOptions( - name='service', - options={'ordering': ['protocol', 'port']}, - ), - migrations.AddField( - model_name='service', - name='virtual_machine', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='virtualization.VirtualMachine'), - ), - migrations.AlterField( - model_name='service', - name='device', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'), - ), - migrations.AlterField( - model_name='ipaddress', - name='role', - field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP'), (44, 'CARP')], help_text='The functional role of this IP', null=True, verbose_name='Role'), - ), - ] diff --git a/netbox/ipam/migrations/0021_vrf_ordering_squashed_0025_custom_tag_models.py b/netbox/ipam/migrations/0021_vrf_ordering_squashed_0025_custom_tag_models.py deleted file mode 100644 index f101f0dd0..000000000 --- a/netbox/ipam/migrations/0021_vrf_ordering_squashed_0025_custom_tag_models.py +++ /dev/null @@ -1,145 +0,0 @@ -import taggit.managers -from django.db import migrations, models - - -class Migration(migrations.Migration): - - replaces = [('ipam', '0021_vrf_ordering'), ('ipam', '0022_tags'), ('ipam', '0023_change_logging'), ('ipam', '0024_vrf_allow_null_rd'), ('ipam', '0025_custom_tag_models')] - - dependencies = [ - ('ipam', '0020_ipaddress_add_role_carp'), - ('taggit', '0002_auto_20150616_2121'), - ('extras', '0019_tag_taggeditem'), - ] - - operations = [ - migrations.AlterModelOptions( - name='vrf', - options={'ordering': ['name', 'rd'], 'verbose_name': 'VRF', 'verbose_name_plural': 'VRFs'}, - ), - migrations.AddField( - model_name='rir', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='rir', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='role', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='role', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='vlangroup', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='vlangroup', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='aggregate', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='aggregate', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='ipaddress', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='ipaddress', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='prefix', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='prefix', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='service', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='service', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='vlan', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='vlan', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='vrf', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='vrf', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AlterField( - model_name='vrf', - name='rd', - field=models.CharField(blank=True, max_length=21, null=True, unique=True), - ), - migrations.AddField( - model_name='aggregate', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='ipaddress', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='prefix', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='service', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='vlan', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - migrations.AddField( - model_name='vrf', - name='tags', - field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), - ), - ] diff --git a/netbox/ipam/migrations/0026_prefix_ordering_vrf_nulls_first_squashed_0032_role_description.py b/netbox/ipam/migrations/0026_prefix_ordering_vrf_nulls_first_squashed_0032_role_description.py deleted file mode 100644 index d5116039d..000000000 --- a/netbox/ipam/migrations/0026_prefix_ordering_vrf_nulls_first_squashed_0032_role_description.py +++ /dev/null @@ -1,140 +0,0 @@ -import django.core.validators -from django.db import migrations, models -import django.db.models.expressions - -PREFIX_STATUS_CHOICES = ( - (0, 'container'), - (1, 'active'), - (2, 'reserved'), - (3, 'deprecated'), -) - -IPADDRESS_STATUS_CHOICES = ( - (0, 'container'), - (1, 'active'), - (2, 'reserved'), - (3, 'deprecated'), -) - -IPADDRESS_ROLE_CHOICES = ( - (10, 'loopback'), - (20, 'secondary'), - (30, 'anycast'), - (40, 'vip'), - (41, 'vrrp'), - (42, 'hsrp'), - (43, 'glbp'), - (44, 'carp'), -) - -VLAN_STATUS_CHOICES = ( - (1, 'active'), - (2, 'reserved'), - (3, 'deprecated'), -) - -SERVICE_PROTOCOL_CHOICES = ( - (6, 'tcp'), - (17, 'udp'), -) - - -def prefix_status_to_slug(apps, schema_editor): - Prefix = apps.get_model('ipam', 'Prefix') - for id, slug in PREFIX_STATUS_CHOICES: - Prefix.objects.filter(status=str(id)).update(status=slug) - - -def ipaddress_status_to_slug(apps, schema_editor): - IPAddress = apps.get_model('ipam', 'IPAddress') - for id, slug in IPADDRESS_STATUS_CHOICES: - IPAddress.objects.filter(status=str(id)).update(status=slug) - - -def ipaddress_role_to_slug(apps, schema_editor): - IPAddress = apps.get_model('ipam', 'IPAddress') - for id, slug in IPADDRESS_ROLE_CHOICES: - IPAddress.objects.filter(role=str(id)).update(role=slug) - - -def vlan_status_to_slug(apps, schema_editor): - VLAN = apps.get_model('ipam', 'VLAN') - for id, slug in VLAN_STATUS_CHOICES: - VLAN.objects.filter(status=str(id)).update(status=slug) - - -def service_protocol_to_slug(apps, schema_editor): - Service = apps.get_model('ipam', 'Service') - for id, slug in SERVICE_PROTOCOL_CHOICES: - Service.objects.filter(protocol=str(id)).update(protocol=slug) - - -class Migration(migrations.Migration): - - replaces = [('ipam', '0026_prefix_ordering_vrf_nulls_first'), ('ipam', '0027_ipaddress_add_dns_name'), ('ipam', '0028_3569_prefix_fields'), ('ipam', '0029_3569_ipaddress_fields'), ('ipam', '0030_3569_vlan_fields'), ('ipam', '0031_3569_service_fields'), ('ipam', '0032_role_description')] - - dependencies = [ - ('ipam', '0025_custom_tag_models'), - ] - - operations = [ - migrations.AlterModelOptions( - name='prefix', - options={'ordering': [django.db.models.expressions.OrderBy(django.db.models.expressions.F('vrf'), nulls_first=True), 'family', 'prefix'], 'verbose_name_plural': 'prefixes'}, - ), - migrations.AddField( - model_name='ipaddress', - name='dns_name', - field=models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, periods, and underscores are allowed in DNS names', regex='^[0-9A-Za-z._-]+$')]), - ), - migrations.AlterField( - model_name='prefix', - name='status', - field=models.CharField(default='active', max_length=50), - ), - migrations.RunPython( - code=prefix_status_to_slug, - ), - migrations.AlterField( - model_name='ipaddress', - name='status', - field=models.CharField(default='active', max_length=50), - ), - migrations.RunPython( - code=ipaddress_status_to_slug, - ), - migrations.AlterField( - model_name='ipaddress', - name='role', - field=models.CharField(blank=True, default='', max_length=50), - ), - migrations.RunPython( - code=ipaddress_role_to_slug, - ), - migrations.AlterField( - model_name='ipaddress', - name='role', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AlterField( - model_name='vlan', - name='status', - field=models.CharField(default='active', max_length=50), - ), - migrations.RunPython( - code=vlan_status_to_slug, - ), - migrations.AlterField( - model_name='service', - name='protocol', - field=models.CharField(max_length=50), - ), - migrations.RunPython( - code=service_protocol_to_slug, - ), - migrations.AddField( - model_name='role', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - ] diff --git a/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py index 9e496153e..8d068df35 100644 --- a/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py +++ b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py @@ -13,8 +13,7 @@ class Migration(migrations.Migration): ] 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. + # Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed. migrations.RunPython( code=ipaddress_status_dhcp_to_slug ), diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index f6ed7901a..84720845e 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -50,7 +50,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel): unique=True, blank=True, null=True, - verbose_name='Route distinguisher' + verbose_name='Route distinguisher', + help_text='Unique route distinguisher (as defined in RFC 4364)' ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -364,7 +365,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description', ] clone_fields = [ 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', @@ -635,7 +636,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', + 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', 'dns_name', 'description', ] clone_fields = [ @@ -925,7 +926,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] + csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description'] clone_fields = [ 'site', 'group', 'tenant', 'status', 'role', 'description', ] @@ -1017,7 +1018,10 @@ class Service(ChangeLoggedModel, CustomFieldModel): choices=ServiceProtocolChoices ) port = models.PositiveIntegerField( - validators=[MinValueValidator(SERVICE_PORT_MIN), MaxValueValidator(SERVICE_PORT_MAX)], + validators=[ + MinValueValidator(SERVICE_PORT_MIN), + MaxValueValidator(SERVICE_PORT_MAX) + ], verbose_name='Port number' ) ipaddresses = models.ManyToManyField( diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 19735b81c..23bf14653 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -3,7 +3,7 @@ from django_tables2.utils import Accessor from dcim.models import Interface from tenancy.tables import COL_TENANT -from utilities.tables import BaseTable, BooleanColumn, ToggleColumn +from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF RIR_UTILIZATION = """ @@ -190,12 +190,23 @@ TENANT_LINK = """ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + rd = tables.Column( + verbose_name='RD' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + enforce_unique = BooleanColumn( + verbose_name='Unique' + ) + tags = TagColumn( + url_name='ipam:vrf_list' + ) class Meta(BaseTable.Meta): model = VRF - fields = ('pk', 'name', 'rd', 'tenant', 'description') + fields = ('pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags') + default_columns = ('pk', 'name', 'rd', 'tenant', 'description') # @@ -204,14 +215,23 @@ class VRFTable(BaseTable): class RIRTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') - is_private = BooleanColumn(verbose_name='Private') - aggregate_count = tables.Column(verbose_name='Aggregates') - actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') + name = tables.LinkColumn() + is_private = BooleanColumn( + verbose_name='Private' + ) + aggregate_count = tables.Column( + verbose_name='Aggregates' + ) + actions = tables.TemplateColumn( + template_code=RIR_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = RIR - fields = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions') + default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') class RIRDetailTable(RIRTable): @@ -247,6 +267,10 @@ class RIRDetailTable(RIRTable): class Meta(RIRTable.Meta): fields = ( + 'pk', 'name', 'slug', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', + 'stats_deprecated', 'stats_available', 'utilization', 'actions', + ) + default_columns = ( 'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions', ) @@ -258,8 +282,13 @@ class RIRDetailTable(RIRTable): class AggregateTable(BaseTable): pk = ToggleColumn() - prefix = tables.LinkColumn(verbose_name='Aggregate') - date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') + prefix = tables.LinkColumn( + verbose_name='Aggregate' + ) + date_added = tables.DateColumn( + format="Y-m-d", + verbose_name='Added' + ) class Meta(BaseTable.Meta): model = Aggregate @@ -267,11 +296,20 @@ class AggregateTable(BaseTable): class AggregateDetailTable(AggregateTable): - child_count = tables.Column(verbose_name='Prefixes') - utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') + child_count = tables.Column( + verbose_name='Prefixes' + ) + utilization = tables.TemplateColumn( + template_code=UTILIZATION_GRAPH, + orderable=False + ) + tags = TagColumn( + url_name='ipam:aggregate_list' + ) class Meta(AggregateTable.Meta): - fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description') + fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description', 'tags') + default_columns = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description') # @@ -300,7 +338,8 @@ class RoleTable(BaseTable): class Meta(BaseTable.Meta): model = Role - fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'slug', 'weight', 'actions') + fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions') + default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') # @@ -309,28 +348,65 @@ class RoleTable(BaseTable): class PrefixTable(BaseTable): pk = ToggleColumn() - prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) - status = tables.TemplateColumn(STATUS_LABEL) - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(template_code=TENANT_LINK) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') - role = tables.TemplateColumn(PREFIX_ROLE_LINK) + prefix = tables.TemplateColumn( + template_code=PREFIX_LINK, + attrs={'th': {'style': 'padding-left: 17px'}} + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + vlan = tables.LinkColumn( + viewname='ipam:vlan', + args=[Accessor('vlan.pk')], + verbose_name='VLAN' + ) + role = tables.TemplateColumn( + template_code=PREFIX_ROLE_LINK + ) + is_pool = BooleanColumn( + verbose_name='Pool' + ) class Meta(BaseTable.Meta): model = Prefix - fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') + fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description') + default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') row_attrs = { 'class': lambda record: 'success' if not record.pk else '', } class PrefixDetailTable(PrefixTable): - utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + utilization = tables.TemplateColumn( + template_code=UTILIZATION_GRAPH, + orderable=False + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + tags = TagColumn( + url_name='ipam:prefix_list' + ) class Meta(PrefixTable.Meta): - fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') + fields = ( + 'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description', + 'tags', + ) + default_columns = ( + 'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', + ) # @@ -339,12 +415,27 @@ class PrefixDetailTable(PrefixTable): class IPAddressTable(BaseTable): pk = ToggleColumn() - address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - status = tables.TemplateColumn(STATUS_LABEL) - tenant = tables.TemplateColumn(template_code=TENANT_LINK) - parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) - interface = tables.Column(orderable=False) + address = tables.TemplateColumn( + template_code=IPADDRESS_LINK, + verbose_name='IP Address' + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) + parent = tables.TemplateColumn( + template_code=IPADDRESS_PARENT, + orderable=False + ) + interface = tables.Column( + orderable=False + ) class Meta(BaseTable.Meta): model = IPAddress @@ -358,22 +449,43 @@ class IPAddressTable(BaseTable): class IPAddressDetailTable(IPAddressTable): nat_inside = tables.LinkColumn( - 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' + viewname='ipam:ipaddress', + args=[Accessor('nat_inside.pk')], + orderable=False, + verbose_name='NAT (Inside)' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + tags = TagColumn( + url_name='ipam:ipaddress_list' ) - tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(IPAddressTable.Meta): fields = ( 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name', - 'description', + 'description', 'tags', + ) + default_columns = ( + 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description', ) class IPAddressAssignTable(BaseTable): - address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address') - status = tables.TemplateColumn(STATUS_LABEL) - parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) - interface = tables.Column(orderable=False) + address = tables.TemplateColumn( + template_code=IPADDRESS_ASSIGN_LINK, + verbose_name='IP Address' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + parent = tables.TemplateColumn( + template_code=IPADDRESS_PARENT, + orderable=False + ) + interface = tables.Column( + orderable=False + ) class Meta(BaseTable.Meta): model = IPAddress @@ -385,10 +497,19 @@ class InterfaceIPAddressTable(BaseTable): """ List IP addresses assigned to a specific Interface. """ - address = tables.LinkColumn(verbose_name='IP Address') - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - status = tables.TemplateColumn(STATUS_LABEL) - tenant = tables.TemplateColumn(template_code=TENANT_LINK) + address = tables.LinkColumn( + verbose_name='IP Address' + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) class Meta(BaseTable.Meta): model = IPAddress @@ -401,16 +522,24 @@ class InterfaceIPAddressTable(BaseTable): class VLANGroupTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - vlan_count = tables.Column(verbose_name='VLANs') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='') + name = tables.LinkColumn() + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + vlan_count = tables.Column( + verbose_name='VLANs' + ) + actions = tables.TemplateColumn( + template_code=VLANGROUP_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = VLANGroup fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions') + default_columns = ('pk', 'name', 'site', 'vlan_count', 'description', 'actions') # @@ -419,12 +548,27 @@ class VLANGroupTable(BaseTable): class VLANTable(BaseTable): pk = ToggleColumn() - vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(VLAN_ROLE_LINK) + vid = tables.TemplateColumn( + template_code=VLAN_LINK, + verbose_name='ID' + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + group = tables.LinkColumn( + viewname='ipam:vlangroup_vlans', + args=[Accessor('group.pk')] + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) class Meta(BaseTable.Meta): model = VLAN @@ -435,16 +579,30 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): - prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + prefixes = tables.TemplateColumn( + template_code=VLAN_PREFIXES, + orderable=False, + verbose_name='Prefixes' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + tags = TagColumn( + url_name='ipam:vlan_list' + ) class Meta(VLANTable.Meta): - fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') + fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') + default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') class VLANMemberTable(BaseTable): - parent = tables.LinkColumn(order_by=['device', 'virtual_machine']) - name = tables.LinkColumn(verbose_name='Interface') + parent = tables.LinkColumn( + order_by=['device', 'virtual_machine'] + ) + name = tables.LinkColumn( + verbose_name='Interface' + ) untagged = tables.TemplateColumn( template_code=VLAN_MEMBER_UNTAGGED, orderable=False @@ -464,13 +622,29 @@ class InterfaceVLANTable(BaseTable): """ List VLANs assigned to a specific Interface. """ - vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') + vid = tables.LinkColumn( + viewname='ipam:vlan', + args=[Accessor('pk')], + verbose_name='ID' + ) tagged = BooleanColumn() - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(VLAN_ROLE_LINK) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + group = tables.Column( + accessor=Accessor('group.name'), + verbose_name='Group' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) class Meta(BaseTable.Meta): model = VLAN @@ -491,7 +665,11 @@ class ServiceTable(BaseTable): viewname='ipam:service', args=[Accessor('pk')] ) + tags = TagColumn( + url_name='ipam:service_list' + ) class Meta(BaseTable.Meta): model = Service - fields = ('pk', 'name', 'parent', 'protocol', 'port', 'description') + fields = ('pk', 'name', 'parent', 'protocol', 'port', 'ipaddresses', 'description', 'tags') + default_columns = ('pk', 'name', 'parent', 'protocol', 'port', 'description') diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index b7089f5f8..785f5f2c5 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -40,6 +40,10 @@ class VRFTestCase(TestCase): ) VRF.objects.bulk_create(vrfs) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['VRF 1', 'VRF 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -86,6 +90,10 @@ class RIRTestCase(TestCase): ) RIR.objects.bulk_create(rirs) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['RIR 1', 'RIR 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -129,6 +137,10 @@ class AggregateTestCase(TestCase): ) Aggregate.objects.bulk_create(aggregates) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_family(self): params = {'family': '4'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -165,8 +177,7 @@ class RoleTestCase(TestCase): Role.objects.bulk_create(roles) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -251,6 +262,10 @@ class PrefixTestCase(TestCase): ) Prefix.objects.bulk_create(prefixes) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_family(self): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) @@ -409,6 +424,10 @@ class IPAddressTestCase(TestCase): ) IPAddress.objects.bulk_create(ipaddresses) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_family(self): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) @@ -531,8 +550,7 @@ class VLANGroupTestCase(TestCase): VLANGroup.objects.bulk_create(vlan_groups) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_name(self): @@ -624,6 +642,10 @@ class VLANTestCase(TestCase): ) VLAN.objects.bulk_create(vlans) + def test_id(self): + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['VLAN 101', 'VLAN 102']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -719,8 +741,7 @@ class ServiceTestCase(TestCase): Service.objects.bulk_create(services) def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:3] - params = {'id': [str(id) for id in id_list]} + params = {'id': self.queryset.values_list('pk', flat=True)[:3]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_name(self): diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 2b9788808..94497f3cd 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -124,6 +124,12 @@ EXEMPT_VIEW_PERMISSIONS = [ # 'ipam.prefix', ] +# HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). +# HTTP_PROXIES = { +# 'http': 'http://10.10.1.10:3128', +# 'https': 'http://10.10.1.10:1080', +# } + # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: # https://docs.djangoproject.com/en/stable/topics/logging/ LOGGING = {} diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index c7116b0af..a4d9ff618 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.8.1' +VERSION = '2.8.2' # Hostname HOSTNAME = platform.node() @@ -77,6 +77,7 @@ DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BAS EMAIL = getattr(configuration, 'EMAIL', {}) ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) +HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) LOGGING = getattr(configuration, 'LOGGING', {}) LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None) diff --git a/netbox/netbox/tests/test_releases.py b/netbox/netbox/tests/test_releases.py index 635a6782b..53a29a260 100644 --- a/netbox/netbox/tests/test_releases.py +++ b/netbox/netbox/tests/test_releases.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import requests from cacheops import CacheMiss, RedisCache +from django.conf import settings from django.test import SimpleTestCase, override_settings from packaging.version import Version from requests import Response @@ -77,7 +78,8 @@ class GetReleasesTestCase(SimpleTestCase): # Check if correct request is made dummy_request_get.assert_called_once_with( 'https://localhost/unittest/releases', - headers={'Accept': 'application/vnd.github.v3+json'} + headers={'Accept': 'application/vnd.github.v3+json'}, + proxies=settings.HTTP_PROXIES ) # Check if result is put in cache @@ -105,7 +107,8 @@ class GetReleasesTestCase(SimpleTestCase): # Check if correct request is made dummy_request_get.assert_called_once_with( 'https://localhost/unittest/releases', - headers={'Accept': 'application/vnd.github.v3+json'} + headers={'Accept': 'application/vnd.github.v3+json'}, + proxies=settings.HTTP_PROXIES ) # Check if result is put in cache @@ -137,7 +140,8 @@ class GetReleasesTestCase(SimpleTestCase): # Check if correct request is made dummy_request_get.assert_called_once_with( 'https://localhost/unittest/releases', - headers={'Accept': 'application/vnd.github.v3+json'} + headers={'Accept': 'application/vnd.github.v3+json'}, + proxies=settings.HTTP_PROXIES ) # Check if failure is put in cache diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 25c32338b..37a516409 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -20,7 +20,7 @@ from dcim.models import ( Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis ) from dcim.tables import ( - CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable, + CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable, ) from extras.models import ObjectChange, ReportResult @@ -44,7 +44,7 @@ SEARCH_TYPES = OrderedDict(( # Circuits ('provider', { 'permission': 'circuits.view_provider', - 'queryset': Provider.objects.all(), + 'queryset': Provider.objects.annotate(count_circuits=Count('circuits')), 'filterset': ProviderFilterSet, 'table': ProviderTable, 'url': 'circuits:provider_list', @@ -93,7 +93,7 @@ SEARCH_TYPES = OrderedDict(( 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', ), 'filterset': DeviceFilterSet, - 'table': DeviceDetailTable, + 'table': DeviceTable, 'url': 'dcim:device_list', }), ('virtualchassis', { diff --git a/netbox/project-static/js/configcontext.js b/netbox/project-static/js/configcontext.js deleted file mode 100644 index 1d731e696..000000000 --- a/netbox/project-static/js/configcontext.js +++ /dev/null @@ -1,11 +0,0 @@ -$('.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 ff520a23f..06d4a742a 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -448,4 +448,33 @@ $(document).ready(function() { $('a.image-preview').on('mouseout', function() { $('#image-preview-window').fadeOut('fast'); }); + + // Rearrange options within a + + + + + + + +
+ {% plugin_buttons virtualchassis %} + {% if perms.dcim.change_virtualchassis %} + {% edit_button virtualchassis %} + {% endif %} + {% if perms.dcim.delete_virtualchassis %} + {% delete_button virtualchassis %} + {% endif %} +
+

{% block title %}{{ virtualchassis }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=virtualchassis %} +
+ {% custom_links virtualchassis %} +
+ +{% endblock %} + +{% block content %} +
+
+
+
+ Virtual Chassis +
+ + + + + +
Domain{{ virtualchassis.domain|placeholder }}
+
+ {% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %} + {% plugin_left_page virtualchassis %} +
+
+
+
+ Members +
+ + + + + + + + {% for vc_member in virtualchassis.members.all %} + + + + + + + {% endfor %} +
DevicePositionMasterPriority
+ {{ vc_member }} + {{ vc_member.vc_position }}{% if virtualchassis.master == vc_member %}{% endif %}{{ vc_member.vc_priority|placeholder }}
+ {% if perms.dcim.change_virtualchassis %} + + {% endif %} +
+ {% plugin_right_page virtualchassis %} +
+
+
+
+ {% plugin_full_width_page virtualchassis %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 998ab7681..21e8cdab6 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -215,13 +215,9 @@ {% include 'extras/inc/configcontext_format.html' %}
- {% include 'extras/inc/configcontext_data.html' with data=configcontext.data %} + {% include 'extras/inc/configcontext_data.html' with data=configcontext.data format=format %}
{% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/templates/extras/inc/configcontext_data.html b/netbox/templates/extras/inc/configcontext_data.html index d91960e2c..085887748 100644 --- a/netbox/templates/extras/inc/configcontext_data.html +++ b/netbox/templates/extras/inc/configcontext_data.html @@ -1,8 +1,5 @@ {% load helpers %} -
-
{{ data|render_json }}
-
-
- {% include 'extras/inc/configcontext_data.html' with data=rendered_context %} + {% include 'extras/inc/configcontext_data.html' with data=rendered_context format=format %}
@@ -24,7 +24,7 @@
{% if obj.local_context_data %} - {% include 'extras/inc/configcontext_data.html' with data=obj.local_context_data %} + {% include 'extras/inc/configcontext_data.html' with data=obj.local_context_data format=format %} {% else %} None {% endif %} @@ -49,7 +49,7 @@ {% if context.description %}
{{ context.description }} {% endif %} - {% include 'extras/inc/configcontext_data.html' with data=context.data %} + {% include 'extras/inc/configcontext_data.html' with data=context.data format=format %}
{% empty %}
@@ -60,7 +60,3 @@
{% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html new file mode 100644 index 000000000..66844c7ca --- /dev/null +++ b/netbox/templates/inc/table_config_form.html @@ -0,0 +1,28 @@ +{% load form_helpers %} + diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index b775af73e..690a966b0 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -1,4 +1,4 @@ -{% extends 'users/_user.html' %} +{% extends 'users/base.html' %} {% load helpers %} {% block title %}API Tokens{% endblock %} @@ -19,7 +19,7 @@ {% endif %} - {{ token.key }} + {{ token.key }} {% if token.is_expired %} Expired {% endif %} @@ -27,24 +27,24 @@
- {{ token.created|date }}
- Created + Created
+ {{ token.created|date }}
+ Expires
{% if token.expires %} - {{ token.expires|date }}
+ {{ token.expires|date }} {% else %} - Never
+ Never {% endif %} - Expires
+ Create/edit/delete operations
{% if token.write_enabled %} Enabled {% else %} Disabled - {% endif %}
- Create/edit/delete operations + {% endif %}
{% if token.description %} diff --git a/netbox/templates/users/_user.html b/netbox/templates/users/base.html similarity index 77% rename from netbox/templates/users/_user.html rename to netbox/templates/users/base.html index 441caf289..15d81ae0f 100644 --- a/netbox/templates/users/_user.html +++ b/netbox/templates/users/base.html @@ -2,16 +2,19 @@ {% block content %}
-
+

{% block title %}{% endblock %}

-
+
-
+
{% block usercontent %}{% endblock %}
diff --git a/netbox/templates/users/change_password.html b/netbox/templates/users/change_password.html index 700bf682d..20c6d048b 100644 --- a/netbox/templates/users/change_password.html +++ b/netbox/templates/users/change_password.html @@ -1,10 +1,10 @@ -{% extends 'users/_user.html' %} +{% extends 'users/base.html' %} {% load form_helpers %} {% block title %}Change Password{% endblock %} {% block usercontent %} -
+ {% csrf_token %} {% if form.non_field_errors %}
diff --git a/netbox/templates/users/preferences.html b/netbox/templates/users/preferences.html new file mode 100644 index 000000000..8b3c4bcc4 --- /dev/null +++ b/netbox/templates/users/preferences.html @@ -0,0 +1,35 @@ +{% extends 'users/base.html' %} +{% load helpers %} + +{% block title %}User Preferences{% endblock %} + +{% block usercontent %} + {% if preferences %} + + {% csrf_token %} + + + + + + + + + + {% for key, value in preferences.items %} + + + + + + {% endfor %} + +
PreferenceValue
{{ key }}{{ value }}
+ + + {% else %} +

No preferences found

+ {% endif %} +{% endblock %} diff --git a/netbox/templates/users/profile.html b/netbox/templates/users/profile.html index 7e7697991..35a94ac6f 100644 --- a/netbox/templates/users/profile.html +++ b/netbox/templates/users/profile.html @@ -1,4 +1,4 @@ -{% extends 'users/_user.html' %} +{% extends 'users/base.html' %} {% load helpers %} {% block title %}User Profile{% endblock %} diff --git a/netbox/templates/users/userkey.html b/netbox/templates/users/userkey.html index e98ec4030..2861d187e 100644 --- a/netbox/templates/users/userkey.html +++ b/netbox/templates/users/userkey.html @@ -1,4 +1,4 @@ -{% extends 'users/_user.html' %} +{% extends 'users/base.html' %} {% block title %}User Key{% endblock %} diff --git a/netbox/templates/users/userkey_edit.html b/netbox/templates/users/userkey_edit.html index 40c3715b0..0715f9038 100644 --- a/netbox/templates/users/userkey_edit.html +++ b/netbox/templates/users/userkey_edit.html @@ -1,4 +1,4 @@ -{% extends 'users/_user.html' %} +{% extends 'users/base.html' %} {% load static %} {% load form_helpers %} diff --git a/netbox/templates/utilities/obj_bulk_import.html b/netbox/templates/utilities/obj_bulk_import.html index a476cbd15..4359d49a6 100644 --- a/netbox/templates/utilities/obj_bulk_import.html +++ b/netbox/templates/utilities/obj_bulk_import.html @@ -3,58 +3,95 @@ {% load form_helpers %} {% block content %} -

{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}

{% block tabs %}{% endblock %} -
-
- {% if form.non_field_errors %} -
-
Errors
-
- {{ form.non_field_errors }} +
+
+

{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}

+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
-
- {% endif %} -
- {% csrf_token %} - {% render_form form %} -
-
- - {% if return_url %} - Cancel + {% endif %} + +
+
+ + {% csrf_token %} + {% render_form form %} +
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
+ +
+

+ {% if fields %} +
+
+ CSV Field Options +
+ + + + + + + + {% for name, field in fields.items %} + + + + + + + {% endfor %} +
FieldRequiredAccessorDescription
+ {{ name }} + + {% if field.required %} + + {% else %} + + {% endif %} + + {% if field.to_field_name %} + {{ field.to_field_name }} + {% else %} + + {% endif %} + + {% if field.help_text %} + {{ field.help_text }}
+ {% elif field.label %} + {{ field.label }}
+ {% endif %} + {% if field|widget_type == 'dateinput' %} + Format: YYYY-MM-DD + {% elif field|widget_type == 'checkboxinput' %} + Specify "true" or "false" + {% endif %} +
+
+

+ Required fields must be specified for all + objects. +

+

+ Related objects may be referenced by any unique attribute. + For example, vrf.rd would identify a VRF by its route distinguisher. +

{% endif %}
- -
-
- {% if fields %} -

CSV Format

- - - - - - - {% for name, field in fields.items %} - - - - - - {% endfor %} -
FieldRequiredDescription
{{ name }}{% if field.required %}{% endif %} - {{ field.help_text|default:field.label }} - {% if field.choices %} -
Choices: {{ field|example_choices }} - {% elif field|widget_type == 'dateinput' %} -
Format: YYYY-MM-DD - {% elif field|widget_type == 'checkboxinput' %} -
Specify "true" or "false" - {% endif %} -
- {% endif %} -
-
+
+
{% endblock %} diff --git a/netbox/templates/utilities/obj_list.html b/netbox/templates/utilities/obj_list.html index f5482baf0..4cfa8b1ce 100644 --- a/netbox/templates/utilities/obj_list.html +++ b/netbox/templates/utilities/obj_list.html @@ -5,6 +5,9 @@ {% block content %}
{% block buttons %}{% endblock %} + {% if table_config_form %} + + {% endif %} {% if permissions.add and 'add' in action_buttons %} {% add_button content_type.model_class|url_name:"add" %} {% endif %} @@ -68,6 +71,9 @@ {% endwith %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+ {% if table_config_form %} + {% include 'inc/table_config_form.html' %} + {% endif %}
{% if filter_form %}
diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html index 8edc83f9c..638220f5a 100644 --- a/netbox/templates/utilities/templatetags/tag.html +++ b/netbox/templates/utilities/templatetags/tag.html @@ -1,5 +1,3 @@ {% load helpers %} -{% if url_name %}{% endif %} -{{ tag }} -{% if url_name %}{% endif %} +{% if url_name %}{% endif %}{{ tag }}{% if url_name %}{% endif %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 7b250e5b1..0ff5e78f4 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -138,7 +138,7 @@ {% if perms.virtualization.change_cluster %}