1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.7

This commit is contained in:
Jeremy Stretch
2020-01-16 09:38:23 -05:00
18 changed files with 288 additions and 52 deletions

2
.github/lock.yml vendored
View File

@ -5,7 +5,7 @@ daysUntilLock: 90
# Skip issues and pull requests created before a given timestamp. Timestamp must # Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable # 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 # Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: [] exemptLabels: []

View File

@ -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. 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 ## 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. 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.

View File

@ -33,6 +33,10 @@ Update the following static libraries to their most recent stable release:
* jQuery * jQuery
* jQuery UI * 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 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`.

View File

@ -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 <app> <start> <end>
```
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.

View File

@ -5,7 +5,7 @@
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger (OpenAPI) * [#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 * [#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 * [#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 * [#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 * [#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 * [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view
@ -30,7 +30,7 @@
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs on the IP address view * [#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 * [#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 * [#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 * [#3905](https://github.com/netbox-community/netbox/issues/3905) - Fix divide-by-zero on power feeds with low power values
--- ---

View File

@ -58,7 +58,7 @@ filesystem on the NetBox server. This release introduces support for several rem
* Amazon S3 * Amazon S3
* ApacheLibcloud * ApacheLibcloud
* Azure Storage * Azure Storage
* DigitalOcean Spaces * netbox-community Spaces
* Dropbox * Dropbox
* FTP * FTP
* Google Cloud Storage * Google Cloud Storage
@ -220,32 +220,37 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be
## Enhancements ## Enhancements
* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields) * [#33](https://github.com/netbox-community/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
* [#648](https://github.com/digitalocean/netbox/issues/648) - Pre-populate forms when selecting "create and add another" * [#648](https://github.com/netbox-community/netbox/issues/648) - Pre-populate forms when selecting "create and add another"
* [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types * [#792](https://github.com/netbox-community/netbox/issues/792) - Add power port and power outlet types
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types * [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types
* [#2669](https://github.com/digitalocean/netbox/issues/2669) - Relax uniqueness constraint on device and VM names * [#2669](https://github.com/netbox-community/netbox/issues/2669) - Relax uniqueness constraint on device and VM names
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace `supervisord` with `systemd` * [#2902](https://github.com/netbox-community/netbox/issues/2902) - Replace `supervisord` with `systemd`
* [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster * [#3455](https://github.com/netbox-community/netbox/issues/3455) - Add tenant assignment to cluster
* [#3520](https://github.com/digitalocean/netbox/issues/3520) - Add Jinja2 template support for Graphs * [#3520](https://github.com/netbox-community/netbox/issues/3520) - Add Jinja2 template support for Graphs
* [#3564](https://github.com/digitalocean/netbox/issues/3564) - Add list views for device components * [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - Introduce a REST API endpoint for executing custom * [#3564](https://github.com/netbox-community/netbox/issues/3564) - Add list views for device components
* [#3538](https://github.com/netbox-community/netbox/issues/3538) - Introduce a REST API endpoint for executing custom
scripts scripts
* [#3655](https://github.com/digitalocean/netbox/issues/3655) - Add `description` field to organizational models * [#3655](https://github.com/netbox-community/netbox/issues/3655) - Add `description` field to organizational models
* [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags * [#3664](https://github.com/netbox-community/netbox/issues/3664) - Enable applying configuration contexts by tags
* [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed * [#3706](https://github.com/netbox-community/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed
* [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field * [#3731](https://github.com/netbox-community/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
* [#3801](https://github.com/digitalocean/netbox/issues/3801) - Use YAML for export of device types * [#3801](https://github.com/netbox-community/netbox/issues/3801) - Use YAML for export of device types
## Bug Fixes ## Bug Fixes
* [#3830](https://github.com/digitalocean/netbox/issues/3830) - Ensure deterministic ordering for all models * [#3830](https://github.com/netbox-community/netbox/issues/3830) - Ensure deterministic ordering for all models
* [#3930](https://github.com/digitalocean/netbox/issues/3930) - Fix API rendering of the `family` field for aggregates * [#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
* [#3919](https://github.com/netbox-community/netbox/issues/3919) - Fix utilization graph extending out of bounds when utilization > 100%
* [#3927](https://github.com/netbox-community/netbox/issues/3927) - Fix exception when deleting devices with secrets assigned
* [#3930](https://github.com/netbox-community/netbox/issues/3930) - Fix API rendering of the `family` field for aggregates
## Bug Fixes (From Beta) ## Bug Fixes (From Beta)
* [#3868](https://github.com/digitalocean/netbox/issues/3868) - Fix creation of interfaces for virtual machines * [#3868](https://github.com/netbox-community/netbox/issues/3868) - Fix creation of interfaces for virtual machines
* [#3878](https://github.com/digitalocean/netbox/issues/3878) - Fix database migration for cable status field * [#3878](https://github.com/netbox-community/netbox/issues/3878) - Fix database migration for cable status field
## API Changes ## API Changes

View File

@ -55,6 +55,7 @@ pages:
- Utility Views: 'development/utility-views.md' - Utility Views: 'development/utility-views.md'
- Extending Models: 'development/extending-models.md' - Extending Models: 'development/extending-models.md'
- Release Checklist: 'development/release-checklist.md' - Release Checklist: 'development/release-checklist.md'
- Squashing Migrations: 'development/squashing-migrations.md'
- Release Notes: - Release Notes:
- Version 2.6: 'release-notes/version-2.6.md' - Version 2.6: 'release-notes/version-2.6.md'
- Version 2.5: 'release-notes/version-2.5.md' - Version 2.5: 'release-notes/version-2.5.md'

View File

@ -11,7 +11,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, F, ProtectedError, Q, Sum from django.db.models import Count, F, ProtectedError, Sum
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey

View File

@ -1,6 +1,6 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from netaddr import AddrFormatError, IPNetwork from netaddr import AddrFormatError, IPNetwork, IPAddress
from . import lookups from . import lookups
from .formfields import IPFormField from .formfields import IPFormField
@ -23,7 +23,10 @@ class BaseIPField(models.Field):
if not value: if not value:
return value return value
try: try:
return IPNetwork(value) if '/' in str(value):
return IPNetwork(value)
else:
return IPAddress(value)
except AddrFormatError as e: except AddrFormatError as e:
raise ValidationError("Invalid IP address format: {}".format(value)) raise ValidationError("Invalid IP address format: {}".format(value))
except (TypeError, ValueError) as e: except (TypeError, ValueError) as e:
@ -32,6 +35,8 @@ class BaseIPField(models.Field):
def get_prep_value(self, value): def get_prep_value(self, value):
if not value: if not value:
return None return None
if isinstance(value, list):
return [str(self.to_python(v)) for v in value]
return str(self.to_python(value)) return str(self.to_python(value))
def form_class(self): def form_class(self):
@ -90,5 +95,6 @@ IPAddressField.register_lookup(lookups.NetContainedOrEqual)
IPAddressField.register_lookup(lookups.NetContains) IPAddressField.register_lookup(lookups.NetContains)
IPAddressField.register_lookup(lookups.NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetContainsOrEquals)
IPAddressField.register_lookup(lookups.NetHost) IPAddressField.register_lookup(lookups.NetHost)
IPAddressField.register_lookup(lookups.NetIn)
IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetHostContained)
IPAddressField.register_lookup(lookups.NetMaskLength) IPAddressField.register_lookup(lookups.NetMaskLength)

View File

@ -7,7 +7,9 @@ from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site from dcim.models import Device, Interface, Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import (
MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .choices import * from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@ -284,7 +286,7 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
method='search_by_parent', method='search_by_parent',
label='Parent prefix', label='Parent prefix',
) )
address = django_filters.CharFilter( address = MultiValueCharFilter(
method='filter_address', method='filter_address',
label='Address', label='Address',
) )
@ -371,13 +373,8 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
return queryset.none() return queryset.none()
def filter_address(self, queryset, name, value): def filter_address(self, queryset, name, value):
if not value.strip():
return queryset
try: try:
# Match address and subnet mask return queryset.filter(address__net_in=value)
if '/' in value:
return queryset.filter(address=value)
return queryset.filter(address__net_host=value)
except ValidationError: except ValidationError:
return queryset.none() return queryset.none()

View File

@ -100,6 +100,42 @@ class NetHost(Lookup):
return 'HOST(%s) = %s' % (lhs, rhs), params return 'HOST(%s) = %s' % (lhs, rhs), params
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)
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:
clause_elements.append(', ')
clause_elements.append('%s')
clause_elements.append(')')
return ''.join(clause_elements)
class NetHostContained(Lookup): 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 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

View File

@ -337,16 +337,19 @@ class IPAddressTestCase(TestCase):
IPAddress(family=4, address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), IPAddress(family=4, address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(family=4, address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), IPAddress(family=4, address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(family=4, address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), IPAddress(family=4, address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
IPAddress(family=4, address='10.0.0.1/25', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, role=None),
IPAddress(family=6, address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), IPAddress(family=6, address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
IPAddress(family=6, address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), IPAddress(family=6, address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(family=6, address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), IPAddress(family=6, address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(family=6, address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), IPAddress(family=6, address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
IPAddress(family=6, address='2001:db8::1/65', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, role=None),
) )
IPAddress.objects.bulk_create(ipaddresses) IPAddress.objects.bulk_create(ipaddresses)
def test_family(self): def test_family(self):
params = {'family': '6'} 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): def test_dns_name(self):
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']} params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
@ -359,20 +362,24 @@ class IPAddressTestCase(TestCase):
def test_parent(self): def test_parent(self):
params = {'parent': '10.0.0.0/24'} 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'} 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 # 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) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'address': '10.0.0.1'} params = {'address': ['10.0.0.1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'address': '2001:db8::1/64'} params = {'address': ['10.0.0.1/24', '10.0.0.1/25']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'address': '2001:db8::1'} params = {'address': ['2001:db8::1/64']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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): def test_mask_length(self):
params = {'mask_length': '24'} params = {'mask_length': '24'}
@ -411,7 +418,7 @@ class IPAddressTestCase(TestCase):
params = {'assigned_to_interface': 'true'} params = {'assigned_to_interface': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'assigned_to_interface': 'false'} 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): def test_status(self):
params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]} params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]}

View File

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
# Environment setup # Environment setup
# #
VERSION = '2.7-beta2' VERSION = '2.7.0-dev'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

@ -15,7 +15,7 @@ $('button.toggle-ips').click(function() {
$('input.interface-filter').on('input', function() { $('input.interface-filter').on('input', function() {
var filter = new RegExp(this.value); 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 // Slice off 'interface_' at the start of the ID
if (filter && filter.test(interface.id.slice(10))) { if (filter && filter.test(interface.id.slice(10))) {
// Match the toggle in case the filter now matches the interface // Match the toggle in case the filter now matches the interface

View File

@ -14,6 +14,7 @@ from django.urls import reverse
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.models import Device
from extras.models import CustomFieldModel, TaggedItem from extras.models import CustomFieldModel, TaggedItem
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from .exceptions import InvalidKey from .exceptions import InvalidKey
@ -346,10 +347,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __str__(self): 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 '{} for {} ({})'.format(self.role, self.device, self.name)
# Return role and device if no name is set # 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 '{} for {}'.format(self.role, self.device)
return 'Secret' return 'Secret'

View File

@ -12,7 +12,7 @@
<li><a href="{{ objectchange.related_object.get_absolute_url }}changelog/">{{ objectchange.related_object }}</a></li> <li><a href="{{ objectchange.related_object.get_absolute_url }}changelog/">{{ objectchange.related_object }}</a></li>
{% elif objectchange.changed_object.get_absolute_url %} {% elif objectchange.changed_object.get_absolute_url %}
<li><a href="{{ objectchange.changed_object.get_absolute_url }}changelog/">{{ objectchange.changed_object }}</a></li> <li><a href="{{ objectchange.changed_object.get_absolute_url }}changelog/">{{ objectchange.changed_object }}</a></li>
{% else %} {% elif objectchange.changed_object %}
<li>{{ objectchange.changed_object }}</li> <li>{{ objectchange.changed_object }}</li>
{% endif %} {% endif %}
<li>{{ objectchange }}</li> <li>{{ objectchange }}</li>
@ -97,7 +97,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% 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 %} {% if related_changes_count > related_changes_table.rows|length %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a> <a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a>

View File

@ -1,7 +1,7 @@
<div class="progress text-center"> <div class="progress text-center">
{% if utilization < 30 %}<span style="font-size: 12px;">{{ utilization }}%</span>{% endif %} {% if utilization < 30 %}<span style="font-size: 12px;">{{ utilization }}%</span>{% endif %}
<div class="progress-bar progress-bar-{% if utilization >= danger_threshold %}danger{% elif utilization >= warning_threshold %}warning{% else %}success{% endif %}" <div class="progress-bar progress-bar-{% if utilization >= danger_threshold %}danger{% elif utilization >= warning_threshold %}warning{% else %}success{% endif %}"
role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ utilization }}%"> 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 %} {% if utilization >= 30 %}{{ utilization }}%{% endif %}
</div> </div>
</div> </div>

View File

@ -45,3 +45,8 @@ eval $COMMAND
COMMAND="${PYTHON} netbox/manage.py collectstatic --no-input" COMMAND="${PYTHON} netbox/manage.py collectstatic --no-input"
echo "Collecting static files ($COMMAND)..." echo "Collecting static files ($COMMAND)..."
eval $COMMAND eval $COMMAND
# Clear all cached data
COMMAND="${PYTHON} netbox/manage.py invalidate all"
echo "Clearing cache data ($COMMAND)..."
eval $COMMAND