From 2e9f21e222f418bcd326ecbf5250a1de81976959 Mon Sep 17 00:00:00 2001 From: kobayashi Date: Fri, 10 Jan 2020 14:06:21 -0500 Subject: [PATCH 01/13] Filter muiltiple ipaddress terms --- docs/release-notes/version-2.6.md | 1 + netbox/ipam/fields.py | 10 ++++++++-- netbox/ipam/filters.py | 14 ++++++-------- netbox/ipam/lookups.py | 19 +++++++++++++++++++ netbox/ipam/tests/test_filters.py | 28 +++++++++++++++++----------- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index d7832a823..4a9a5522b 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -11,6 +11,7 @@ * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations * [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view * [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace +* [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable ipaddress filtering with multiple address terms * [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation * [#3668](https://github.com/netbox-community/netbox/issues/3668) - Search by DNS name when assigning IP address * [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 1ddf545ea..612340607 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -1,6 +1,6 @@ from django.core.exceptions import ValidationError from django.db import models -from netaddr import AddrFormatError, IPNetwork +from netaddr import AddrFormatError, IPNetwork, IPAddress from . import lookups from .formfields import IPFormField @@ -23,7 +23,10 @@ class BaseIPField(models.Field): if not value: return value try: - return IPNetwork(value) + if '/' in str(value): + return IPNetwork(value) + else: + return IPAddress(value) except AddrFormatError as e: raise ValidationError("Invalid IP address format: {}".format(value)) except (TypeError, ValueError) as e: @@ -32,6 +35,8 @@ class BaseIPField(models.Field): def get_prep_value(self, value): if not value: return None + if isinstance(value, list): + return [str(self.to_python(v)) for v in value] return str(self.to_python(value)) def form_class(self): @@ -90,5 +95,6 @@ IPAddressField.register_lookup(lookups.NetContainedOrEqual) IPAddressField.register_lookup(lookups.NetContains) IPAddressField.register_lookup(lookups.NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetHost) +IPAddressField.register_lookup(lookups.NetHostIn) IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetMaskLength) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 9d1b1d650..934ded93c 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError from dcim.models import Device, Interface, Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filtersets import TenancyFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, MultiValueCharFilter from virtualization.models import VirtualMachine from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -284,7 +284,7 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt method='search_by_parent', label='Parent prefix', ) - address = django_filters.CharFilter( + address = MultiValueCharFilter( method='filter_address', label='Address', ) @@ -371,13 +371,11 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt return queryset.none() def filter_address(self, queryset, name, value): - if not value.strip(): - return queryset try: - # Match address and subnet mask - if '/' in value: - return queryset.filter(address=value) - return queryset.filter(address__net_host=value) + return queryset.filter( + Q(address__in=value) | + Q(address__net_host_in=value) + ) except ValidationError: return queryset.none() diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 54825d9de..c5b6081f1 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -100,6 +100,25 @@ class NetHost(Lookup): return 'HOST(%s) = %s' % (lhs, rhs), params +class NetHostIn(Lookup): + lookup_name = 'net_host_in' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + in_elements = ['HOST(%s) IN (' % lhs] + params = [] + for offset in range(0, len(rhs_params[0])): + if offset > 0: + in_elements.append(', ') + params.extend(lhs_params) + sqls_params = rhs_params[0][offset] + in_elements.append(rhs) + params.append(sqls_params) + in_elements.append(')') + return ''.join(in_elements), params + + class NetHostContained(Lookup): """ Check for the host portion of an IP address without regard to its mask. This allows us to find e.g. 192.0.2.1/24 diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index 5ae912bc6..492b42a8b 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -337,16 +337,18 @@ class IPAddressTestCase(TestCase): IPAddress(family=4, address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPADDRESS_STATUS_ACTIVE, role=None, dns_name='ipaddress-b'), IPAddress(family=4, address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPADDRESS_STATUS_RESERVED, role=IPADDRESS_ROLE_VIP, dns_name='ipaddress-c'), IPAddress(family=4, address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPADDRESS_STATUS_DEPRECATED, role=IPADDRESS_ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(family=4, address='10.0.0.1/25', vrf=None, interface=None, status=IPADDRESS_STATUS_ACTIVE, role=None), IPAddress(family=6, address='2001:db8::1/64', vrf=None, interface=None, status=IPADDRESS_STATUS_ACTIVE, role=None, dns_name='ipaddress-a'), IPAddress(family=6, address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPADDRESS_STATUS_ACTIVE, role=None, dns_name='ipaddress-b'), IPAddress(family=6, address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPADDRESS_STATUS_RESERVED, role=IPADDRESS_ROLE_VIP, dns_name='ipaddress-c'), IPAddress(family=6, address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPADDRESS_STATUS_DEPRECATED, role=IPADDRESS_ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(family=6, address='2001:db8::1/65', vrf=None, interface=None, status=IPADDRESS_STATUS_ACTIVE, role=None), ) IPAddress.objects.bulk_create(ipaddresses) def test_family(self): params = {'family': '6'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) def test_dns_name(self): params = {'dns_name': ['ipaddress-a', 'ipaddress-b']} @@ -359,20 +361,24 @@ class IPAddressTestCase(TestCase): def test_parent(self): params = {'parent': '10.0.0.0/24'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) params = {'parent': '2001:db8::/64'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) - def filter_address(self): + def test_filter_address(self): # Check IPv4 and IPv6, with and without a mask - params = {'address': '10.0.0.1/24'} + params = {'address': ['10.0.0.1/24']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'address': '10.0.0.1'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'address': '2001:db8::1/64'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'address': '2001:db8::1'} + params = {'address': ['10.0.0.1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'address': ['10.0.0.1/24', '10.0.0.1/25']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'address': ['2001:db8::1/64']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'address': ['2001:db8::1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'address': ['2001:db8::1/64', '2001:db8::1/65']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_mask_length(self): params = {'mask_length': '24'} @@ -411,7 +417,7 @@ class IPAddressTestCase(TestCase): params = {'assigned_to_interface': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'assigned_to_interface': 'false'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_status(self): params = {'status': [PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED]} From e3aacb183b2604e0c06cb5ad7fd1748e7b752578 Mon Sep 17 00:00:00 2001 From: kobayashi Date: Sun, 12 Jan 2020 16:44:15 -0500 Subject: [PATCH 02/13] optimize query --- netbox/ipam/fields.py | 2 +- netbox/ipam/filters.py | 5 +---- netbox/ipam/lookups.py | 41 +++++++++++++++++++++++++++++------------ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 612340607..845820432 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -95,6 +95,6 @@ IPAddressField.register_lookup(lookups.NetContainedOrEqual) IPAddressField.register_lookup(lookups.NetContains) IPAddressField.register_lookup(lookups.NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetHost) -IPAddressField.register_lookup(lookups.NetHostIn) +IPAddressField.register_lookup(lookups.NetIn) IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetMaskLength) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 934ded93c..e9baf98cc 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -372,10 +372,7 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt def filter_address(self, queryset, name, value): try: - return queryset.filter( - Q(address__in=value) | - Q(address__net_host_in=value) - ) + return queryset.filter(address__net_in=value) except ValidationError: return queryset.none() diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index c5b6081f1..457cd7734 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -100,23 +100,40 @@ class NetHost(Lookup): return 'HOST(%s) = %s' % (lhs, rhs), params -class NetHostIn(Lookup): - lookup_name = 'net_host_in' +class NetIn(Lookup): + lookup_name = 'net_in' def as_sql(self, qn, connection): lhs, lhs_params = self.process_lhs(qn, connection) rhs, rhs_params = self.process_rhs(qn, connection) - in_elements = ['HOST(%s) IN (' % lhs] - params = [] - for offset in range(0, len(rhs_params[0])): + with_mask, without_mask = [], [] + for address in rhs_params[0]: + if '/' in address: + with_mask.append(address) + else: + without_mask.append(address) + + address_in_clause = self.create_in_clause('{} IN ('.format(lhs), len(with_mask)) + host_in_clause = self.create_in_clause('HOST({}) IN ('.format(lhs), len(without_mask)) + + if with_mask and not without_mask: + return address_in_clause, with_mask + elif not with_mask and without_mask: + return host_in_clause, without_mask + + in_clause = '({}) OR ({})'.format(address_in_clause, host_in_clause) + with_mask.extend(without_mask) + return in_clause, with_mask + + @staticmethod + def create_in_clause(clause_part, max_size): + clause_elements = [clause_part] + for offset in range(0, max_size): if offset > 0: - in_elements.append(', ') - params.extend(lhs_params) - sqls_params = rhs_params[0][offset] - in_elements.append(rhs) - params.append(sqls_params) - in_elements.append(')') - return ''.join(in_elements), params + clause_elements.append(', ') + clause_elements.append('%s') + clause_elements.append(')') + return ''.join(clause_elements) class NetHostContained(Lookup): From 7b8e82f32183b3477f79d7c0d6daaba11ebf3e18 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 13 Jan 2020 17:30:16 -0500 Subject: [PATCH 03/13] Fix typo in release notes --- docs/release-notes/version-2.6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index d77fbd365..2c7e0fe41 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -5,7 +5,7 @@ * [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger (OpenAPI) * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering over the link * [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers -* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle the display of child prefixes/IP addresses +* [#2598](https://github.com/netbox-community/netbox/issues/2598) - Toggle the display of child prefixes/IP addresses * [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address to interfaces * [#3021](https://github.com/netbox-community/netbox/issues/3021) - Add `tenant` filter field for cables * [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view From 9d0da0f45a23df63ec66e3d5237538184af8a4c7 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Tue, 14 Jan 2020 06:08:19 +0000 Subject: [PATCH 04/13] Fixes #3914: Interface filter field when unauthenticated --- docs/release-notes/version-2.6.md | 8 ++++++++ netbox/project-static/js/interface_toggles.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 2c7e0fe41..53acd3f45 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,3 +1,11 @@ +# v2.6.13 (FUTURE) + +## Bug Fixes + +* [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated + +--- + # v2.6.12 (2020-01-13) ## Enhancements diff --git a/netbox/project-static/js/interface_toggles.js b/netbox/project-static/js/interface_toggles.js index a3649558a..a46d3185c 100644 --- a/netbox/project-static/js/interface_toggles.js +++ b/netbox/project-static/js/interface_toggles.js @@ -15,7 +15,7 @@ $('button.toggle-ips').click(function() { $('input.interface-filter').on('input', function() { var filter = new RegExp(this.value); - for (interface of $(this).closest('form').find('tbody > tr')) { + for (interface of $(this).closest('div.panel').find('tbody > tr')) { // Slice off 'interface_' at the start of the ID if (filter && filter.test(interface.id.slice(10))) { // Match the toggle in case the filter now matches the interface From a9e1e7fc789317384e8f4e173dab7b46bc9afedd Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Tue, 14 Jan 2020 18:23:31 +0000 Subject: [PATCH 05/13] Fixes #3919: Utilization graph bar bounds --- docs/release-notes/version-2.6.md | 3 ++- netbox/templates/utilities/templatetags/utilization_graph.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 792e8990a..a5930f60c 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -7,6 +7,7 @@ ## Bug Fixes * [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated +* [#3919](https://github.com/netbox-community/netbox/issues/3919) - Fix utilization graph extending out of bounds when utilization > 100% --- @@ -42,7 +43,7 @@ * [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs on the IP address view * [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fix minimum/maximum value rendering for site ASN field * [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group -* [#3898](https://github.com/netbox-community/netbox/issues/3898) - Fix references to deleted cables without a label +* [#3898](https://github.com/netbox-community/netbox/issues/3898) - Fix references to deleted cables without a label * [#3905](https://github.com/netbox-community/netbox/issues/3905) - Fix divide-by-zero on power feeds with low power values --- diff --git a/netbox/templates/utilities/templatetags/utilization_graph.html b/netbox/templates/utilities/templatetags/utilization_graph.html index 9232da3b8..b9b074f20 100644 --- a/netbox/templates/utilities/templatetags/utilization_graph.html +++ b/netbox/templates/utilities/templatetags/utilization_graph.html @@ -1,7 +1,7 @@
{% if utilization < 30 %}{{ utilization }}%{% endif %}
+ role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {% if utilization > 100 %}100{% else %}{{ utilization }}{% endif %}%"> {% if utilization >= 30 %}{{ utilization }}%{% endif %}
From 823e1280d2483ced5b4c9af45345346e3f9abb1d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 14 Jan 2020 14:14:54 -0500 Subject: [PATCH 06/13] Add guide for squashing schema migrations --- docs/development/release-checklist.md | 4 + docs/development/squashing-migrations.md | 168 +++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 173 insertions(+) create mode 100644 docs/development/squashing-migrations.md diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 008647b8e..60be6bf61 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -33,6 +33,10 @@ 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`. diff --git a/docs/development/squashing-migrations.md b/docs/development/squashing-migrations.md new file mode 100644 index 000000000..bc0c0548f --- /dev/null +++ b/docs/development/squashing-migrations.md @@ -0,0 +1,168 @@ +# 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/mkdocs.yml b/mkdocs.yml index b493a799b..bb41ec2f6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ pages: - Utility Views: 'development/utility-views.md' - Extending Models: 'development/extending-models.md' - Release Checklist: 'development/release-checklist.md' + - Squashing Migrations: 'development/squashing-migrations.md' - Release Notes: - Version 2.6: 'release-notes/version-2.6.md' - Version 2.5: 'release-notes/version-2.5.md' From e0ea5b0e0be31f063be48b9f21814f6478e0fa66 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 08:49:50 -0500 Subject: [PATCH 07/13] Allow the Lock bot to lock existing closed issues --- .github/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/lock.yml b/.github/lock.yml index 36a41b04e..e00f3f4db 100644 --- a/.github/lock.yml +++ b/.github/lock.yml @@ -5,7 +5,7 @@ daysUntilLock: 90 # Skip issues and pull requests created before a given timestamp. Timestamp must # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: 2020-01-01 +skipCreatedBefore: false # Issues and pull requests with these labels will be ignored. Set to `[]` to disable exemptLabels: [] From 685cf50268858c3ea211907ba63a69638c9cef8f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 08:57:00 -0500 Subject: [PATCH 08/13] Closes #3926: Extend upgrade script to invalidate cache data --- upgrade.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/upgrade.sh b/upgrade.sh index d17dec06d..2ff585e8d 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -45,3 +45,8 @@ eval $COMMAND COMMAND="${PYTHON} netbox/manage.py collectstatic --no-input" echo "Collecting static files ($COMMAND)..." eval $COMMAND + +# Clear all cached data +COMMAND="${PYTHON} netbox/manage.py invalidate all" +echo "Clearing cache data ($COMMAND)..." +eval $COMMAND From 1ea820a50e46cf76eb0f6f14f5ca1f4434b5dd55 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 10:23:07 -0500 Subject: [PATCH 09/13] Fixes #3900: Fix exception when deleting device types --- docs/release-notes/version-2.6.md | 1 + netbox/dcim/models.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 792e8990a..e3ad87ad0 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -6,6 +6,7 @@ ## Bug Fixes +* [#3900](https://github.com/netbox-community/netbox/issues/3900) - Fix exception when deleting device types * [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated --- diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 833fb483b..8ab3a37a3 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -38,11 +38,18 @@ class ComponentTemplateModel(models.Model): raise NotImplementedError() def to_objectchange(self, action): + # Annotate the parent DeviceType + try: + parent = getattr(self, 'device_type', None) + except ObjectDoesNotExist: + # The parent DeviceType has already been deleted + parent = None + return ObjectChange( changed_object=self, object_repr=str(self), action=action, - related_object=self.device_type, + related_object=parent, object_data=serialize_object(self) ) From dda9a2ee1c51c4512ce96e230fdf1ee41107c180 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 10:39:23 -0500 Subject: [PATCH 10/13] Fixes #3927: Fix exception when deleting devices with secrets assigned --- docs/release-notes/version-2.6.md | 1 + netbox/secrets/models.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index e3ad87ad0..4d81061a5 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -8,6 +8,7 @@ * [#3900](https://github.com/netbox-community/netbox/issues/3900) - Fix exception when deleting device types * [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated +* [#3927](https://github.com/netbox-community/netbox/issues/3927) - Fix exception when deleting devices with secrets assigned --- diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 6dcb5abee..e8de8cbd3 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -14,6 +14,7 @@ from django.urls import reverse from django.utils.encoding import force_bytes from taggit.managers import TaggableManager +from dcim.models import Device from extras.models import CustomFieldModel, TaggedItem from utilities.models import ChangeLoggedModel from .exceptions import InvalidKey @@ -359,10 +360,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel): super().__init__(*args, **kwargs) def __str__(self): - if self.role and self.device and self.name: + try: + device = self.device + except Device.DoesNotExist: + device = None + if self.role and device and self.name: return '{} for {} ({})'.format(self.role, self.device, self.name) # Return role and device if no name is set - if self.role and self.device: + if self.role and device: return '{} for {}'.format(self.role, self.device) return 'Secret' From 0053aa2d2e8d0a94e9eb026888c7e2f104a24b1e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 10:44:31 -0500 Subject: [PATCH 11/13] Fix objectchange related changes panel styling --- netbox/templates/extras/objectchange.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index fca33506a..70b66dcea 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -97,7 +97,7 @@
- {% include 'panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='noprint' %} + {% include 'panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %} {% if related_changes_count > related_changes_table.rows|length %}
See all {{ related_changes_count|add:"1" }} changes From e5ebe6cebc0e7ae5f6c2f98a90a4d7b302c71904 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 10:48:30 -0500 Subject: [PATCH 12/13] Fix breadcrumbs for changelog entries for deleted objects --- netbox/templates/extras/objectchange.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index 70b66dcea..ee29281f9 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -12,7 +12,7 @@
  • {{ objectchange.related_object }}
  • {% elif objectchange.changed_object.get_absolute_url %}
  • {{ objectchange.changed_object }}
  • - {% else %} + {% elif objectchange.changed_object %}
  • {{ objectchange.changed_object }}
  • {% endif %}
  • {{ objectchange }}
  • From 0893d326656bec0bce1ef79c8004e5c797040b94 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Jan 2020 13:27:46 -0500 Subject: [PATCH 13/13] Clarify naming constraints related to rack groups --- docs/core-functionality/sites-and-racks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md index bf3c473fd..f86e24b3e 100644 --- a/docs/core-functionality/sites-and-racks.md +++ b/docs/core-functionality/sites-and-racks.md @@ -40,6 +40,8 @@ Racks can be arranged into groups. As with sites, how you choose to designate ra Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported. +The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.) + ## Rack Roles Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.