Compare commits

...

48 Commits

Author SHA1 Message Date
Jeremy Stretch
9cc4992fad Merge pull request #7018 from netbox-community/develop
Release v2.11.12
2021-08-23 15:36:04 -04:00
jeremystretch
6518d87200 Release v2.11.12 2021-08-23 15:16:42 -04:00
jeremystretch
8497965cf7 Fixes #6326: Enable filtering assigned VLANs by group in interface edit form 2021-08-23 12:49:32 -04:00
jeremystretch
0b0ab9277c Fixes #6776: Fix erroneous webhook dispatch on failure to save objects 2021-08-23 12:06:43 -04:00
jeremystretch
75c62ff729 Print request index after webhook data dump 2021-08-23 11:32:47 -04:00
Jeremy Stretch
aef8c5fbb5 Merge pull request #6965 from bluikko/poweroutlet-hardwired
Add hardwired PowerOutlet
2021-08-23 10:04:28 -04:00
jeremystretch
cfa4f5677b Fixes #7012: Fix hidden "add components" dropdown on devices list 2021-08-23 09:41:43 -04:00
jeremystretch
8131feae8a Closes #7011: Add search field to VM interfaces filter form 2021-08-23 09:36:05 -04:00
jeremystretch
1fc3c6d9d2 Fixes #6974: Show contextual label for IP address role 2021-08-20 16:12:09 -04:00
jeremystretch
53a5bc2221 Fixes #6929: Introduce LOGIN_PERSISTENCE configuration parameter to persist user sessions 2021-08-20 16:06:37 -04:00
jeremystretch
d850aa0773 Changelog for #6790 2021-08-20 09:17:53 -04:00
Jeremy Stretch
9baebfa241 Merge pull request #6790 from WillIrvine/issue-6632
Fixes #6632  - Allow a /32 prefix to contain a /32 ipaddress
2021-08-20 09:16:05 -04:00
jeremystretch
10847e2956 Optimize addition/removal of default custom field values 2021-08-16 14:48:56 -04:00
jeremystretch
9b0258fef4 Fixes #6686: Force assignment of null custom field values to objects 2021-08-16 14:38:06 -04:00
jeremystretch
5b89cdc868 Fixes #5968: Model forms should save empty custom field values as null 2021-08-16 13:45:46 -04:00
bluikko
5a8cedd63f Add hardwired PowerOutlet 2021-08-16 11:30:13 +07:00
jeremystretch
3feba2997f Closes #6872: Add table configuration button to child prefixes view 2021-08-13 15:56:14 -04:00
jeremystretch
fce419526d Closes #6748: Add site group filter to devices list 2021-08-13 15:26:06 -04:00
jeremystretch
1b12185a39 PRVB 2021-08-12 11:47:59 -04:00
Jeremy Stretch
2e895c734e Merge pull request #6947 from netbox-community/develop
Release v2.11.11
2021-08-12 11:44:51 -04:00
Jeremy Stretch
11a9dc57fc Merge branch 'master' into develop 2021-08-12 11:31:29 -04:00
jeremystretch
badd92a50e Update GitHub issue templates 2021-08-12 11:28:55 -04:00
jeremystretch
b2faf8044d Release v2.11.11 2021-08-12 11:22:57 -04:00
jeremystretch
3105e9545a Fixes #6918: Fix return URL persistence when adding multiple objects sequentially 2021-08-12 10:12:42 -04:00
jeremystretch
42c71984f9 Fixes #6896: Fix validation of IP address assigned as device/VM primary via NAT relation 2021-08-11 21:15:45 -04:00
jeremystretch
db359719a9 Closes #6921: Employ a sandbox when rendering Jinja2 code for increased security 2021-08-10 20:52:45 -04:00
jeremystretch
7bceeb714b Fixes #6935: Remove extraneous columns from inventory item and device bay tables 2021-08-10 20:35:39 -04:00
jeremystretch
35b8fc6e83 Fixes #6936: Add missing parent column to inventory item import form 2021-08-10 20:24:57 -04:00
jeremystretch
1bb596fc7e Fixes #6908: Allow assignment of scope to VLAN groups upon import 2021-08-09 09:54:27 -04:00
jeremystretch
7bcebd5b0f Fixes #6910: Fix exception on invalid CSV import column name 2021-08-09 09:20:22 -04:00
jeremystretch
a8b6902829 Fixes #6909: Remove extraneous site column from VLAN group import form 2021-08-09 09:17:08 -04:00
Jeremy Stretch
71e6dc8275 Merge pull request #6920 from candlerb/candlerb/6919
Change example ADMINS to show a tuple
2021-08-09 08:56:44 -04:00
Jeremy Stretch
564640213e Merge pull request #6915 from candlerb/candlerb/libpq-dev
Documentation consistency on installation of libpq-dev(el)
2021-08-09 08:51:54 -04:00
Brian Candler
b04f262642 Change example ADMINS to show a tuple
Fixes #6919
2021-08-09 07:37:46 +01:00
Brian Candler
b802127801 Documentation consistency on installation of libpq-dev(el) 2021-08-08 10:19:30 +01:00
jeremystretch
f23dc2d405 Fixes #6902: Populate device field when cloning device components 2021-08-06 09:55:47 -04:00
jeremystretch
7c8612aadd Update application architecture diagram 2021-08-05 15:51:24 -04:00
Joel McGuire
d347b97f20 Fixes #6887 Add Examples in the Lookup Expression Docs (#6898)
Fixes #6887 Add Examples in the Lookup Expression Docs

Co-authored-by: joel <joelmcguire@email.arizona.edu>
2021-08-05 13:28:32 -04:00
jeremystretch
46d0af6cef Fixes #6892: Fix validation of unit ranges when creating a rack reservation 2021-08-05 11:12:08 -04:00
jeremystretch
ee8fd701ae Changelog for #6883 2021-08-04 13:26:53 -04:00
Jeremy Stretch
9379324b07 Merge pull request #6885 from bellwood/6883_add_power_outlet_port_c21_c22
Add power outlet/port choice for C21/C22
2021-08-04 12:59:21 -04:00
Brian Ellwood
55cdbd57cc Add power outlet/port choice for C21/C22
Resolves #6883
2021-08-04 12:06:39 -04:00
jeremystretch
76df55dfc0 Fixes #6740: Add import button to VM interfaces list 2021-07-30 10:28:56 -04:00
Jeremy Stretch
49a949aa97 Merge pull request #6836 from Ursadon/patch-1
Escaping angle brackets in a device config file
2021-07-30 10:09:04 -04:00
jeremystretch
288bf477ce Bump GitHub stale action to v4.0 2021-07-29 09:07:23 -04:00
Ursadon
27f3816fc6 Escaping angle brackets in a device config file
The configuration file may contain brackets (">" or "<"), which must be escaped
2021-07-29 15:45:32 +07:00
jeremystretch
18a4232783 PRVB 2021-07-28 16:00:38 -04:00
WillIrvine
ffae2c5f18 Fixes #6632 2021-07-23 11:08:41 +12:00
45 changed files with 388 additions and 100 deletions

View File

@@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.10
placeholder: v2.11.12
validations:
required: true
- type: dropdown

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v2.11.10
placeholder: v2.11.12
validations:
required: true
- type: dropdown

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v4
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an

View File

@@ -17,6 +17,9 @@ When viewing a device named Router4, this link would render as:
Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
!!! warning
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
## Context Data
The following context data is available within the template when rendering a custom link's text or URL.

View File

@@ -4,10 +4,13 @@ NetBox allows users to define custom templates that can be used when exporting o
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
!!! note
The name `table` is reserved for internal use.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
!!! warning
Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users.
The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:

View File

@@ -2,6 +2,9 @@
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks.
!!! warning
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
## Configuration
* **Name** - A unique name for the webhook. The name is not included with outbound messages.

View File

@@ -257,6 +257,16 @@ LOGGING = {
---
## LOGIN_PERSISTENCE
Default: False
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
---
## LOGIN_REQUIRED
Default: False

View File

@@ -11,13 +11,13 @@ This section entails the installation and configuration of a local PostgreSQL da
```no-highlight
sudo apt update
sudo apt install -y postgresql libpq-dev
sudo apt install -y postgresql
```
=== "CentOS"
```no-highlight
sudo yum install -y postgresql-server libpq-devel
sudo yum install -y postgresql-server
sudo postgresql-setup --initdb
```

View File

@@ -18,7 +18,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
=== "CentOS"
```no-highlight
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
```
Before continuing with either platform, update pip (Python's package management tool) to its latest release:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,5 +1,48 @@
# NetBox v2.11
## v2.11.12 (2021-08-23)
### Enhancements
* [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list
* [#6790](https://github.com/netbox-community/netbox/issues/6790) - Recognize a /32 IPv4 address as a child of a /32 IPv4 prefix
* [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view
* [#6929](https://github.com/netbox-community/netbox/issues/6929) - Introduce `LOGIN_PERSISTENCE` configuration parameter to persist user sessions
* [#7011](https://github.com/netbox-community/netbox/issues/7011) - Add search field to VM interfaces filter form
### Bug Fixes
* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
* [#6326](https://github.com/netbox-community/netbox/issues/6326) - Enable filtering assigned VLANs by group in interface edit form
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list
---
## v2.11.11 (2021-08-12)
### Enhancements
* [#6883](https://github.com/netbox-community/netbox/issues/6883) - Add C21 & C22 power types
* [#6921](https://github.com/netbox-community/netbox/issues/6921) - Employ a sandbox when rendering Jinja2 code for increased security
### Bug Fixes
* [#6740](https://github.com/netbox-community/netbox/issues/6740) - Add import button to VM interfaces list
* [#6892](https://github.com/netbox-community/netbox/issues/6892) - Fix validation of unit ranges when creating a rack reservation
* [#6896](https://github.com/netbox-community/netbox/issues/6896) - Fix validation of IP address assigned as device/VM primary via NAT relation
* [#6902](https://github.com/netbox-community/netbox/issues/6902) - Populate device field when cloning device components
* [#6908](https://github.com/netbox-community/netbox/issues/6908) - Allow assignment of scope to VLAN groups upon import
* [#6909](https://github.com/netbox-community/netbox/issues/6909) - Remove extraneous `site` column from VLAN group import form
* [#6910](https://github.com/netbox-community/netbox/issues/6910) - Fix exception on invalid CSV import column name
* [#6918](https://github.com/netbox-community/netbox/issues/6918) - Fix return URL persistence when adding multiple objects sequentially
* [#6935](https://github.com/netbox-community/netbox/issues/6935) - Remove extraneous columns from inventory item and device bay tables
* [#6936](https://github.com/netbox-community/netbox/issues/6936) - Add missing `parent` column to inventory item import form
---
## v2.11.10 (2021-07-28)
### Enhancements

View File

@@ -69,6 +69,12 @@ Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
| `gt` | Greater than |
| `gte` | Greater than or equal to |
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
```no-highlight
GET /api/ipam/vlans/?vid__gt=900
```
### String Fields
String based (char) fields (Name, Address, etc) support these lookup expressions:
@@ -86,7 +92,17 @@ String based (char) fields (Name, Address, etc) support these lookup expressions
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty (boolean) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
```no-highlight
GET /api/dcim/devices/?name__ic=switch
```
### Foreign Keys & Other Fields
Certain other fields, namely foreign key relationships support just the negation
expression: `n`.
expression: `n`. Here is an example of a lookup expression on a foreign key, it would return all the VLANs that don't have a VLAN Group ID of 3203:
```no-highlight
GET /api/ipam/vlans/?group_id__n=3203
```

View File

@@ -252,6 +252,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_C14 = 'iec-60320-c14'
TYPE_IEC_C16 = 'iec-60320-c16'
TYPE_IEC_C20 = 'iec-60320-c20'
TYPE_IEC_C22 = 'iec-60320-c22'
# IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@@ -351,6 +352,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_C14, 'C14'),
(TYPE_IEC_C16, 'C16'),
(TYPE_IEC_C20, 'C20'),
(TYPE_IEC_C22, 'C22'),
)),
('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
@@ -467,6 +469,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_C13 = 'iec-60320-c13'
TYPE_IEC_C15 = 'iec-60320-c15'
TYPE_IEC_C19 = 'iec-60320-c19'
TYPE_IEC_C21 = 'iec-60320-c21'
# IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@@ -550,6 +553,8 @@ class PowerOutletTypeChoices(ChoiceSet):
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'
CHOICES = (
('IEC 60320', (
@@ -558,6 +563,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_C13, 'C13'),
(TYPE_IEC_C15, 'C15'),
(TYPE_IEC_C19, 'C19'),
(TYPE_IEC_C21, 'C21'),
)),
('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
@@ -650,6 +656,9 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)),
)

View File

@@ -18,7 +18,7 @@ from extras.forms import (
)
from extras.models import Tag
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from ipam.models import IPAddress, VLAN
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
@@ -2421,8 +2421,8 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
model = Device
field_order = [
'q', 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id',
'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
]
q = forms.CharField(
required=False,
@@ -2433,11 +2433,17 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
@@ -3103,15 +3109,26 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
'type': 'lag',
}
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Untagged VLAN'
label='Untagged VLAN',
query_params={
'group_id': '$vlan_group',
}
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Tagged VLANs'
label='Tagged VLANs',
query_params={
'group_id': '$vlan_group',
}
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
@@ -3852,11 +3869,32 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
to_field_name='name',
required=False
)
parent = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
required=False,
help_text='Parent inventory item'
)
class Meta:
model = InventoryItem
fields = InventoryItem.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit parent choices to inventory items belonging to this device
device = None
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
pass
if device:
self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
else:
self.fields['parent'].queryset = InventoryItem.objects.none()
class InventoryItemBulkCreateForm(
form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),

View File

@@ -230,6 +230,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
)
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
clone_fields = ['device', 'type', 'speed']
class Meta:
ordering = ('device', '_name')
@@ -273,6 +274,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
)
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
clone_fields = ['device', 'type', 'speed']
class Meta:
ordering = ('device', '_name')
@@ -324,6 +326,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
csv_headers = [
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
]
clone_fields = ['device', 'maximum_draw', 'allocated_draw']
class Meta:
ordering = ('device', '_name')
@@ -434,6 +437,7 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
)
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description']
clone_fields = ['device', 'type', 'power_port', 'feed_leg']
class Meta:
ordering = ('device', '_name')
@@ -577,6 +581,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
'mgmt_only', 'description', 'mode',
]
clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only']
class Meta:
ordering = ('device', CollateAsChar('_name'))
@@ -711,6 +716,7 @@ class FrontPort(ComponentModel, CableTermination):
csv_headers = [
'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
]
clone_fields = ['device', 'type']
class Meta:
ordering = ('device', '_name')
@@ -767,6 +773,7 @@ class RearPort(ComponentModel, CableTermination):
MaxValueValidator(REARPORT_POSITIONS_MAX)
]
)
clone_fields = ['device', 'type', 'positions']
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description']
@@ -818,6 +825,7 @@ class DeviceBay(ComponentModel):
)
csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
clone_fields = ['device']
class Meta:
ordering = ('device', '_name')
@@ -913,6 +921,7 @@ class InventoryItem(MPTTModel, ComponentModel):
csv_headers = [
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
]
clone_fields = ['device', 'parent', 'manufacturer', 'part_id']
class Meta:
ordering = ('device__id', 'parent__id', '_name')

View File

@@ -232,10 +232,6 @@ class DeviceComponentTable(BaseTable):
linkify=True,
order_by=('_name',)
)
cable = tables.Column(
linkify=True
)
mark_connected = BooleanColumn()
class Meta(BaseTable.Meta):
order_by = ('device', 'name')

View File

@@ -1736,10 +1736,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
"device,name",
"Device 1,Inventory Item 4",
"Device 1,Inventory Item 5",
"Device 1,Inventory Item 6",
"device,name,parent",
"Device 1,Inventory Item 4,Inventory Item 1",
"Device 1,Inventory Item 5,Inventory Item 2",
"Device 1,Inventory Item 6,Inventory Item 3",
)

View File

@@ -2,7 +2,7 @@ from contextlib import contextmanager
from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.signals import _handle_changed_object, _handle_deleted_object
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
from utilities.utils import curry
from .webhooks import flush_webhooks
@@ -20,11 +20,13 @@ def change_logging(request):
# Curry signals receivers to pass the current request
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
yield
@@ -33,6 +35,7 @@ def change_logging(request):
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
# Flush queued webhooks to RQ
flush_webhooks(webhook_queue)

View File

@@ -77,7 +77,11 @@ class CustomFieldModelForm(forms.ModelForm):
# Save custom field data on instance
for cf_name in self.custom_fields:
self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
key = cf_name[3:] # Strip "cf_" from field name
value = self.cleaned_data.get(cf_name)
empty_values = self.fields[cf_name].empty_values
# Convert "empty" values to null
self.instance.custom_field_data[key] = value if value not in empty_values else None
return super().clean()

View File

@@ -37,12 +37,10 @@ class WebhookHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(b'Webhook received!\n')
request_counter += 1
# Print the request headers to stdout
# Print the request headers
if self.show_headers:
for k, v in self.headers.items():
print('{}: {}'.format(k, v))
print(f'{k}: {v}')
print()
# Print the request body (if any)
@@ -55,8 +53,11 @@ class WebhookHandler(BaseHTTPRequestHandler):
else:
print('(No body)')
print(f'Completed request #{request_counter}')
print('------------')
request_counter += 1
class Command(BaseCommand):
help = "Start a simple listener to display received HTTP requests"

View File

@@ -120,6 +120,30 @@ class CustomField(BigIDModel):
# Cache instance's original name so we can check later whether it has changed
self._name = self.name
def populate_initial_data(self, content_types):
"""
Populate initial custom field data upon either a) the creation of a new CustomField, or
b) the assignment of an existing CustomField to new object types.
"""
for ct in content_types:
model = ct.model_class()
instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
for instance in instances:
instance.custom_field_data[self.name] = self.default
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is
no longer assigned to a model, or because it has been deleted).
"""
for ct in content_types:
model = ct.model_class()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
for instance in instances:
del(instance.custom_field_data[self.name])
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
@@ -132,17 +156,6 @@ class CustomField(BigIDModel):
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is
no longer assigned to a model, or because it has been deleted).
"""
for ct in content_types:
model = ct.model_class()
for obj in model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False}):
del(obj.custom_field_data[self.name])
obj.save()
def clean(self):
super().clean()

View File

@@ -1,3 +1,4 @@
import logging
import random
from datetime import timedelta
@@ -6,6 +7,7 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import Signal
from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter
@@ -19,6 +21,10 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
# Change logging/webhooks
#
# Define a custom signal that can be sent to clear any queued webhooks
clear_webhooks = Signal()
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
"""
Fires when an object is created or updated.
@@ -104,10 +110,28 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
model_deletes.labels(instance._meta.model_name).inc()
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
"""
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
"""
logger = logging.getLogger('webhooks')
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
webhook_queue.clear()
#
# Custom fields
#
def handle_cf_added_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the population of default/null values when a CustomField is added to one or more ContentTypes.
"""
if action == 'post_add':
instance.populate_initial_data(ContentType.objects.filter(pk__in=pk_set))
def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
@@ -131,9 +155,10 @@ def handle_cf_deleted(instance, **kwargs):
instance.remove_stale_data(instance.content_types.all())
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField)
m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through)
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
#

View File

@@ -42,8 +42,11 @@ class CustomFieldTest(TestCase):
cf.save()
cf.content_types.set([obj_type])
# Assign a value to the first Site
# Check that the field has a null initial value
site = Site.objects.first()
self.assertIsNone(site.custom_field_data[cf.name])
# Assign a value to the first Site
site.custom_field_data[cf.name] = data['field_value']
site.save()
@@ -73,8 +76,11 @@ class CustomFieldTest(TestCase):
cf.save()
cf.content_types.set([obj_type])
# Assign a value to the first Site
# Check that the field has a null initial value
site = Site.objects.first()
self.assertIsNone(site.custom_field_data[cf.name])
# Assign a value to the first Site
site.custom_field_data[cf.name] = 'Option A'
site.save()

View File

@@ -0,0 +1,53 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.forms import SiteForm
from dcim.models import Site
from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField
class CustomFieldModelFormTest(TestCase):
@classmethod
def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site)
CHOICES = ('A', 'B', 'C')
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
cf_text.content_types.set([obj_type])
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
cf_integer.content_types.set([obj_type])
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
cf_boolean.content_types.set([obj_type])
cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE)
cf_date.content_types.set([obj_type])
cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL)
cf_url.content_types.set([obj_type])
cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES)
cf_select.content_types.set([obj_type])
cf_multiselect = CustomField.objects.create(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT,
choices=CHOICES)
cf_multiselect.content_types.set([obj_type])
def test_empty_values(self):
"""
Test that empty custom field values are stored as null
"""
form = SiteForm({
'name': 'Site 1',
'slug': 'site-1',
'status': 'active',
})
self.assertTrue(form.is_valid())
instance = form.save()
for field_type, _ in CustomFieldTypeChoices.CHOICES:
self.assertIn(field_type, instance.custom_field_data)
self.assertIsNone(instance.custom_field_data[field_type])

View File

@@ -11,8 +11,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, ContentTypeChoiceField, CSVChoiceField,
CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
NumericArrayField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
CSVContentTypeField, CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ExpandableIPAddressField, NumericArrayField, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@@ -682,7 +682,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
# IP addresses
#
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
@@ -1238,17 +1238,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
class VLANGroupCSVForm(CustomFieldModelCSVForm):
site = CSVModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned site'
)
slug = SlugField()
scope_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False,
label='Scope type (app & model)'
)
class Meta:
model = VLANGroup
fields = VLANGroup.csv_headers
labels = {
'scope_id': 'Scope ID',
}
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@@ -151,7 +151,7 @@ class NetHostContained(Lookup):
lhs, lhs_params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params + rhs_params
return 'CAST(HOST(%s) AS INET) << %s' % (lhs, rhs), params
return 'CAST(HOST(%s) AS INET) <<= %s' % (lhs, rhs), params
class NetFamily(Transform):

View File

@@ -649,18 +649,15 @@ class IPAddress(PrimaryModel):
# Check for primary IP assignment that doesn't match the assigned device/VM
if self.pk:
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if device:
if getattr(self.assigned_object, 'device', None) != device:
raise ValidationError({
'interface': f"IP address is primary for device {device} but not assigned to it!"
})
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if vm:
if getattr(self.assigned_object, 'virtual_machine', None) != vm:
raise ValidationError({
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!"
})
for cls, attr in ((Device, 'device'), (VirtualMachine, 'virtual_machine')):
parent = cls.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if parent and getattr(self.assigned_object, attr) != parent:
# Check for a NAT relationship
if not self.nat_inside or getattr(self.nat_inside.assigned_object, attr) != parent:
raise ValidationError({
'interface': f"IP address is primary for {cls._meta.model_name} {parent} but "
f"not assigned to it!"
})
# Validate IP status selection
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:

View File

@@ -333,10 +333,10 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
}
cls.csv_data = (
"name,slug,description",
"VLAN Group 4,vlan-group-4,Fourth VLAN group",
"VLAN Group 5,vlan-group-5,Fifth VLAN group",
"VLAN Group 6,vlan-group-6,Sixth VLAN group",
f"name,slug,scope_type,scope_id,description",
f"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
)
cls.bulk_edit_data = {

View File

@@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from dcim.models import Device, Interface
from netbox.views import generic
from utilities.forms import TableConfigForm
from utilities.tables import paginate_table
from utilities.utils import count_related
from virtualization.models import VirtualMachine, VMInterface
@@ -412,7 +413,7 @@ class PrefixPrefixesView(generic.ObjectView):
if child_prefixes and request.GET.get('show_available', 'true') == 'true':
child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes, user=request.user)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.columns.show('pk')
paginate_table(prefix_table, request)
@@ -433,6 +434,7 @@ class PrefixPrefixesView(generic.ObjectView):
'bulk_querystring': bulk_querystring,
'active_tab': 'prefixes',
'show_available': request.GET.get('show_available', 'true') == 'true',
'table_config_form': TableConfigForm(table=prefix_table),
}

View File

@@ -69,7 +69,7 @@ SECRET_KEY = ''
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
# application errors (assuming correct email settings are provided).
ADMINS = [
# ['John Doe', 'jdoe@example.com'],
# ('John Doe', 'jdoe@example.com'),
]
# URL schemes that are allowed within links in NetBox
@@ -149,6 +149,10 @@ INTERNAL_IPS = ('127.0.0.1', '::1')
# https://docs.djangoproject.com/en/stable/topics/logging/
LOGGING = {}
# Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain
# authenticated to NetBox indefinitely.
LOGIN_PERSISTENCE = False
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = False

View File

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
VERSION = '2.11.10'
VERSION = '2.11.12'
# Hostname
HOSTNAME = platform.node()
@@ -103,6 +103,7 @@ NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
PLUGINS = getattr(configuration, 'PLUGINS', [])
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
@@ -251,6 +252,7 @@ CACHING_REDIS_SKIP_TLS_VERIFY = CACHING_REDIS.get('INSECURE_SKIP_TLS_VERIFY', Fa
if LOGIN_TIMEOUT is not None:
# Django default is 1209600 seconds (14 days)
SESSION_COOKIE_AGE = LOGIN_TIMEOUT
SESSION_SAVE_EVERY_REQUEST = bool(LOGIN_PERSISTENCE)
if SESSION_FILE_PATH is not None:
SESSION_ENGINE = 'django.contrib.sessions.backends.file'

View File

@@ -17,6 +17,7 @@ from django.views.generic import View
from django_tables2.export import TableExport
from extras.models import CustomField, ExportTemplate
from extras.signals import clear_webhooks
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortTransaction, PermissionsViolation
from utilities.forms import (
@@ -306,24 +307,26 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
messages.success(request, mark_safe(msg))
if '_addanother' in request.POST:
redirect_url = request.path
return_url = request.GET.get('return_url')
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
redirect_url = f'{redirect_url}?return_url={return_url}'
# If the object has clone_fields, pre-populate a new instance of the form
if hasattr(obj, 'clone_fields'):
url = '{}?{}'.format(request.path, prepare_cloned_fields(obj))
return redirect(url)
redirect_url += f"{'&' if return_url else '?'}{prepare_cloned_fields(obj)}"
return redirect(request.get_full_path())
return redirect(redirect_url)
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, obj))
return_url = self.get_return_url(request, obj)
return redirect(return_url)
except PermissionsViolation:
msg = "Object save failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -602,12 +605,13 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
raise ObjectDoesNotExist
except AbortTransaction:
pass
clear_webhooks.send(sender=self)
except PermissionsViolation:
msg = "Object creation failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
if not model_form.errors:
logger.info(f"Import object {obj} (PK: {obj.pk})")
@@ -675,7 +679,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
)
def clean(self):
csv_rows = self.cleaned_data['csv'][1]
csv_rows = self.cleaned_data['csv'][1] if 'csv' in self.cleaned_data else None
csv_file = self.files.get('csv_file')
# Check that the user has not submitted both text data and a file
@@ -750,12 +754,13 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
except ValidationError:
pass
clear_webhooks.send(sender=self)
except PermissionsViolation:
msg = "Object import failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -878,11 +883,13 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
except ValidationError as e:
messages.error(self.request, "{} failed validation: {}".format(obj, e))
clear_webhooks.send(sender=self)
except PermissionsViolation:
msg = "Object update failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -986,6 +993,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
msg = "Object update failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
@@ -1182,6 +1190,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
msg = "Component creation failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
return render(request, self.template_name, {
'component_type': self.queryset.model._meta.verbose_name,
@@ -1263,12 +1272,13 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
raise PermissionsViolation
except IntegrityError:
pass
clear_webhooks.send(sender=self)
except PermissionsViolation:
msg = "Component creation failed due to object-level permissions violation"
logger.debug(msg)
form.add_error(None, msg)
clear_webhooks.send(sender=self)
if not form.errors:
msg = "Added {} {} to {} {}.".format(

View File

@@ -337,22 +337,26 @@ $(document).ready(function() {
$('select#id_untagged_vlan').trigger('change');
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_vlan_group').parent().parent().hide();
$('select#id_untagged_vlan').parent().parent().hide();
$('select#id_tagged_vlans').parent().parent().hide();
}
else if ($(this).val() == 'access') {
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_vlan_group').parent().parent().show();
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().hide();
}
else if ($(this).val() == 'tagged') {
$('select#id_vlan_group').parent().parent().show();
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().show();
}
else if ($(this).val() == 'tagged-all') {
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_vlan_group').parent().parent().show();
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().hide();
}

View File

@@ -39,9 +39,9 @@ $(document).ready(function() {
url: "{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config",
dataType: 'json',
success: function(json) {
$('#running_config').html($.trim(json['get_config']['running']));
$('#startup_config').html($.trim(json['get_config']['startup']));
$('#candidate_config').html($.trim(json['get_config']['candidate']));
$('#running_config').text($.trim(json['get_config']['running']));
$('#startup_config').text($.trim(json['get_config']['startup']));
$('#candidate_config').text($.trim(json['get_config']['candidate']));
},
error: function(xhr) {
alert(xhr.responseText);

View File

@@ -2,7 +2,7 @@
{% block bulk_buttons %}
{% if perms.dcim.change_device %}
<div class="btn-group">
<div class="btn-group dropup">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>

View File

@@ -33,6 +33,7 @@
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
<div class="panel-body">
{% render_field form.mode %}
{% render_field form.vlan_group %}
{% render_field form.untagged_vlan %}
{% render_field form.tagged_vlans %}
</div>

View File

@@ -56,7 +56,7 @@
<td>Role</td>
<td>
{% if object.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}" class="label label-{{ object.get_role_class }}">{{ object.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@@ -1,7 +1,12 @@
{% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% load static %}
{% block buttons %}
{% include 'ipam/inc/toggle_available.html' %}
{% if request.user.is_authenticated and table_config_form %}
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#PrefixDetailTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
{% endif %}
{% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-success">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Child Prefix
@@ -22,4 +27,9 @@
{% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
</div>
</div>
{% table_config_form prefix_table table_name="PrefixDetailTable" %}
{% endblock %}
{% block javascript %}
<script src="{% static 'js/tableconfig.js' %}"></script>
{% endblock %}

View File

@@ -28,6 +28,7 @@
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
<div class="panel-body">
{% render_field form.mode %}
{% render_field form.vlan_group %}
{% render_field form.untagged_vlan %}
{% render_field form.tagged_vlans %}
</div>

View File

@@ -269,6 +269,8 @@ class CSVContentTypeField(CSVModelChoiceField):
return f'{value.app_label}.{value.model}'
def to_python(self, value):
if not value:
return None
try:
app_label, model = value.split('.')
except ValueError:

View File

@@ -32,7 +32,10 @@ def parse_numeric_range(string, base=10):
begin, end = dash_range.split('-')
except ValueError:
begin, end = dash_range, dash_range
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
try:
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
except ValueError:
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
values.extend(range(begin, end))
return list(set(values))
@@ -64,7 +67,7 @@ def parse_alphanumeric_range(string):
else:
# Not a valid range (more than a single character)
if not len(begin) == len(end) == 1:
raise forms.ValidationError('Range "{}" is invalid.'.format(dash_range))
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
for n in list(range(ord(begin), ord(end) + 1)):
values.append(chr(n))
return values

View File

@@ -6,7 +6,7 @@ from itertools import count, groupby
from django.core.serializers import serialize
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
from jinja2 import Environment
from jinja2.sandbox import SandboxedEnvironment
from mptt.models import MPTTModel
from dcim.choices import CableLengthUnitChoices
@@ -213,7 +213,7 @@ def render_jinja2(template_code, context):
"""
Render a Jinja2 template with the provided context. Return the rendered content.
"""
return Environment().from_string(source=template_code).render(**context)
return SandboxedEnvironment().from_string(source=template_code).render(**context)
def prepare_cloned_fields(instance):

View File

@@ -12,7 +12,7 @@ from extras.forms import (
CustomFieldFilterForm,
)
from extras.models import Tag
from ipam.models import IPAddress, VLAN
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
@@ -616,15 +616,26 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
required=False,
label='Parent interface'
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Untagged VLAN'
label='Untagged VLAN',
query_params={
'group_id': '$vlan_group',
}
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Tagged VLANs'
label='Tagged VLANs',
query_params={
'group_id': '$vlan_group',
}
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
@@ -834,6 +845,10 @@ class VMInterfaceBulkRenameForm(BulkRenameForm):
class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
model = VMInterface
q = forms.CharField(
required=False,
label=_('Search')
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,

View File

@@ -414,7 +414,7 @@ class VMInterfaceListView(generic.ObjectListView):
filterset = filtersets.VMInterfaceFilterSet
filterset_form = forms.VMInterfaceFilterForm
table = tables.VMInterfaceTable
action_buttons = ('export',)
action_buttons = ('import', 'export')
class VMInterfaceView(generic.ObjectView):

View File

@@ -1,9 +1,9 @@
Django==3.2.5
Django==3.2.6
django-cacheops==6.0
django-cors-headers==3.7.0
django-debug-toolbar==3.2.1
django-cors-headers==3.8.0
django-debug-toolbar==3.2.2
django-filter==2.4.0
django-mptt==0.12.0
django-mptt==0.13.1
django-pglocks==1.0.4
django-prometheus==2.1.0
django-rq==2.4.1