mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'feature' of https://github.com/netbox-community/netbox into feature
# Conflicts: # netbox/project-static/js/forms.js # netbox/templates/dcim/location.html # netbox/templates/generic/object_list.html
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
about: Report a reproducible bug in the current release of NetBox
|
description: Report a reproducible bug in the current release of NetBox
|
||||||
labels: ["type: bug"]
|
labels: ["type: bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 📖 Documentation Change
|
name: 📖 Documentation Change
|
||||||
about: Suggest an addition or modification to the NetBox documentation
|
description: Suggest an addition or modification to the NetBox documentation
|
||||||
labels: ["type: documentation"]
|
labels: ["type: documentation"]
|
||||||
body:
|
body:
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
about: Propose a new NetBox feature or enhancement
|
description: Propose a new NetBox feature or enhancement
|
||||||
labels: ["type: feature"]
|
labels: ["type: feature"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
2
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 🏡 Housekeeping
|
name: 🏡 Housekeeping
|
||||||
about: A change pertaining to the codebase itself (developers only)
|
description: A change pertaining to the codebase itself (developers only)
|
||||||
labels: ["type: housekeeping"]
|
labels: ["type: housekeeping"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -5,7 +5,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.6, 3.7, 3.8]
|
python-version: [3.7, 3.8]
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
|
@ -170,14 +170,9 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
|
|||||||
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
|
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
|
||||||
|
|
||||||
* `model` - The model class
|
* `model` - The model class
|
||||||
* `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
|
|
||||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||||
|
|
||||||
!!! warning
|
|
||||||
The `display_field` parameter is now deprecated, and will be removed in NetBox v2.12. All ObjectVar instances will
|
|
||||||
instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
|
|
||||||
|
|
||||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -288,7 +283,6 @@ class NewBranchScript(Script):
|
|||||||
switch_model = ObjectVar(
|
switch_model = ObjectVar(
|
||||||
description="Access switch model",
|
description="Access switch model",
|
||||||
model=DeviceType,
|
model=DeviceType,
|
||||||
display_field='model',
|
|
||||||
query_params={
|
query_params={
|
||||||
'manufacturer_id': '$manufacturer'
|
'manufacturer_id': '$manufacturer'
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
version-2.11.md
|
version-2.12.md
|
@ -1,41 +1,25 @@
|
|||||||
# NetBox v2.11
|
# NetBox v2.11
|
||||||
|
|
||||||
## v2.11.0 (FUTURE)
|
## v2.11.1 (2021-04-21)
|
||||||
|
|
||||||
### Enhancements (from Beta)
|
### Enhancements
|
||||||
|
|
||||||
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
|
* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
|
||||||
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
|
* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
|
||||||
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
|
* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
|
||||||
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
|
* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
|
||||||
* [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table
|
* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
|
||||||
* [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces
|
|
||||||
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
|
|
||||||
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
|
|
||||||
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
|
|
||||||
* [#6150](https://github.com/netbox-community/netbox/issues/6150) - Enable change logging for journal entries
|
|
||||||
|
|
||||||
### Bug Fixes (from Beta)
|
### Bug Fixes
|
||||||
|
|
||||||
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
|
* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
|
||||||
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
|
* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
|
||||||
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
|
* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
|
||||||
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
|
* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
|
||||||
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
|
|
||||||
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
|
|
||||||
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
|
|
||||||
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
|
|
||||||
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
|
|
||||||
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
|
|
||||||
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
|
|
||||||
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
|
|
||||||
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v2.11-beta1 (2021-04-06)
|
## v2.11.0 (2021-04-16)
|
||||||
|
|
||||||
**WARNING:** This is a beta release and is not suitable for production use. It is intended for development and evaluation purposes only. No upgrade path to the final v2.11 release will be provided from this beta, and users should assume that all data entered into the application will be lost.
|
|
||||||
|
|
||||||
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required.
|
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required.
|
||||||
|
|
||||||
@ -126,7 +110,9 @@ A new provider network model has been introduced to represent the boundary of a
|
|||||||
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
|
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
|
||||||
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
|
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
|
||||||
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
|
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
|
||||||
|
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
|
||||||
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
|
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
|
||||||
|
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
|
||||||
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
|
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
|
||||||
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
|
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
|
||||||
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
|
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
|
||||||
@ -136,6 +122,29 @@ A new provider network model has been introduced to represent the boundary of a
|
|||||||
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
|
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
|
||||||
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
|
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
|
||||||
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
|
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
|
||||||
|
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
|
||||||
|
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
|
||||||
|
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
|
||||||
|
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
|
||||||
|
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
|
||||||
|
|
||||||
|
### Bug Fixes (from v2.11-beta1)
|
||||||
|
|
||||||
|
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
|
||||||
|
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
|
||||||
|
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
|
||||||
|
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
|
||||||
|
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
|
||||||
|
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
|
||||||
|
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
|
||||||
|
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
|
||||||
|
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
|
||||||
|
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
|
||||||
|
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
|
||||||
|
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
|
||||||
|
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
|
||||||
|
* [#6176](https://github.com/netbox-community/netbox/issues/6176) - Correct position of MAC address field when creating VM interfaces
|
||||||
|
* [#6177](https://github.com/netbox-community/netbox/issues/6177) - Prevent VM interface from being assigned as its own parent
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
8
docs/release-notes/version-2.12.md
Normal file
8
docs/release-notes/version-2.12.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# NetBox v2.12
|
||||||
|
|
||||||
|
## v2.12-beta1 (FUTURE)
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
* [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6
|
||||||
|
* [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar
|
@ -209,6 +209,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
|||||||
model = Location
|
model = Location
|
||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RegionFilterForm(BootstrapMixin, forms.Form):
|
class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Site
|
model = Site
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -287,8 +287,8 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupFilterForm(BootstrapMixin, forms.Form):
|
class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Site
|
model = SiteGroup
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Search')
|
label=_('Search')
|
||||||
@ -557,7 +557,12 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class LocationFilterForm(BootstrapMixin, forms.Form):
|
class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = Location
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -2424,10 +2429,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
|||||||
location_id = DynamicModelMultipleChoiceField(
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Location'),
|
null_option='None',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site_id'
|
'site_id': '$site_id'
|
||||||
}
|
},
|
||||||
|
label=_('Location')
|
||||||
)
|
)
|
||||||
rack_id = DynamicModelMultipleChoiceField(
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
|
@ -291,6 +291,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
class DeviceConsolePortTable(ConsolePortTable):
|
class DeviceConsolePortTable(ConsolePortTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -335,6 +336,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-console-network-outline"></i> '
|
template_code='<i class="mdi mdi-console-network-outline"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -379,6 +381,7 @@ class DevicePowerPortTable(PowerPortTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
|
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
|
||||||
'{{ value }}</a>',
|
'{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -428,6 +431,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
|||||||
class DevicePowerOutletTable(PowerOutletTable):
|
class DevicePowerOutletTable(PowerOutletTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -492,6 +496,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
|
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
|
||||||
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
|
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
|
||||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
parent = tables.Column(
|
parent = tables.Column(
|
||||||
@ -555,6 +560,7 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -602,6 +608,7 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -651,6 +658,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
|
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
|
||||||
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
@ -698,6 +706,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
||||||
'{{ value }}</a>',
|
'{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
|
@ -364,16 +364,30 @@ class LocationView(generic.ObjectView):
|
|||||||
queryset = Location.objects.all()
|
queryset = Location.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
location_ids = instance.get_descendants(include_self=True).values_list('pk', flat=True)
|
||||||
location=instance
|
rack_count = Rack.objects.filter(location__in=location_ids).count()
|
||||||
)
|
device_count = Device.objects.filter(location__in=location_ids).count()
|
||||||
|
|
||||||
devices_table = tables.DeviceTable(devices)
|
child_locations = Location.objects.add_related_count(
|
||||||
devices_table.columns.hide('location')
|
Location.objects.add_related_count(
|
||||||
paginate_table(devices_table, request)
|
Location.objects.all(),
|
||||||
|
Device,
|
||||||
|
'location',
|
||||||
|
'device_count',
|
||||||
|
cumulative=True
|
||||||
|
),
|
||||||
|
Rack,
|
||||||
|
'location',
|
||||||
|
'rack_count',
|
||||||
|
cumulative=True
|
||||||
|
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
||||||
|
child_locations_table = tables.LocationTable(child_locations)
|
||||||
|
paginate_table(child_locations_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'devices_table': devices_table,
|
'rack_count': rack_count,
|
||||||
|
'device_count': device_count,
|
||||||
|
'child_locations_table': child_locations_table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1305,8 +1319,7 @@ class DeviceConsolePortsView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
consoleport_table = tables.DeviceConsolePortTable(
|
consoleport_table = tables.DeviceConsolePortTable(
|
||||||
data=consoleports,
|
data=consoleports,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
||||||
consoleport_table.columns.show('pk')
|
consoleport_table.columns.show('pk')
|
||||||
@ -1330,8 +1343,7 @@ class DeviceConsoleServerPortsView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
consoleserverport_table = tables.DeviceConsoleServerPortTable(
|
consoleserverport_table = tables.DeviceConsoleServerPortTable(
|
||||||
data=consoleserverports,
|
data=consoleserverports,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_consoleserverport') or \
|
if request.user.has_perm('dcim.change_consoleserverport') or \
|
||||||
request.user.has_perm('dcim.delete_consoleserverport'):
|
request.user.has_perm('dcim.delete_consoleserverport'):
|
||||||
@ -1354,8 +1366,7 @@ class DevicePowerPortsView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
powerport_table = tables.DevicePowerPortTable(
|
powerport_table = tables.DevicePowerPortTable(
|
||||||
data=powerports,
|
data=powerports,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
||||||
powerport_table.columns.show('pk')
|
powerport_table.columns.show('pk')
|
||||||
@ -1377,8 +1388,7 @@ class DevicePowerOutletsView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
poweroutlet_table = tables.DevicePowerOutletTable(
|
poweroutlet_table = tables.DevicePowerOutletTable(
|
||||||
data=poweroutlets,
|
data=poweroutlets,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
||||||
poweroutlet_table.columns.show('pk')
|
poweroutlet_table.columns.show('pk')
|
||||||
@ -1402,8 +1412,7 @@ class DeviceInterfacesView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
interface_table = tables.DeviceInterfaceTable(
|
interface_table = tables.DeviceInterfaceTable(
|
||||||
data=interfaces,
|
data=interfaces,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
||||||
interface_table.columns.show('pk')
|
interface_table.columns.show('pk')
|
||||||
@ -1425,8 +1434,7 @@ class DeviceFrontPortsView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
frontport_table = tables.DeviceFrontPortTable(
|
frontport_table = tables.DeviceFrontPortTable(
|
||||||
data=frontports,
|
data=frontports,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
||||||
frontport_table.columns.show('pk')
|
frontport_table.columns.show('pk')
|
||||||
@ -1446,8 +1454,7 @@ class DeviceRearPortsView(generic.ObjectView):
|
|||||||
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable')
|
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable')
|
||||||
rearport_table = tables.DeviceRearPortTable(
|
rearport_table = tables.DeviceRearPortTable(
|
||||||
data=rearports,
|
data=rearports,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
||||||
rearport_table.columns.show('pk')
|
rearport_table.columns.show('pk')
|
||||||
@ -1469,8 +1476,7 @@ class DeviceDeviceBaysView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
devicebay_table = tables.DeviceDeviceBayTable(
|
devicebay_table = tables.DeviceDeviceBayTable(
|
||||||
data=devicebays,
|
data=devicebays,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
||||||
devicebay_table.columns.show('pk')
|
devicebay_table.columns.show('pk')
|
||||||
@ -1492,8 +1498,7 @@ class DeviceInventoryView(generic.ObjectView):
|
|||||||
).prefetch_related('manufacturer')
|
).prefetch_related('manufacturer')
|
||||||
inventoryitem_table = tables.DeviceInventoryItemTable(
|
inventoryitem_table = tables.DeviceInventoryItemTable(
|
||||||
data=inventoryitems,
|
data=inventoryitems,
|
||||||
user=request.user,
|
user=request.user
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
||||||
inventoryitem_table.columns.show('pk')
|
inventoryitem_table.columns.show('pk')
|
||||||
|
@ -180,27 +180,16 @@ class ObjectVar(ScriptVariable):
|
|||||||
A single object within NetBox.
|
A single object within NetBox.
|
||||||
|
|
||||||
:param model: The NetBox model being referenced
|
:param model: The NetBox model being referenced
|
||||||
:param display_field: The attribute of the returned object to display in the selection list (DEPRECATED)
|
|
||||||
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
|
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
|
||||||
:param null_option: The label to use as a "null" selection option (optional)
|
:param null_option: The label to use as a "null" selection option (optional)
|
||||||
"""
|
"""
|
||||||
form_field = DynamicModelChoiceField
|
form_field = DynamicModelChoiceField
|
||||||
|
|
||||||
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs):
|
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs):
|
||||||
|
|
||||||
# TODO: Remove display_field in v2.12
|
|
||||||
if 'display_field' in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The 'display_field' parameter has been deprecated, and will be removed in NetBox v2.12. Object "
|
|
||||||
"variables will now reference the 'display' attribute available on all model serializers by default."
|
|
||||||
)
|
|
||||||
display_field = kwargs.pop('display_field', 'display')
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.field_attrs.update({
|
self.field_attrs.update({
|
||||||
'queryset': model.objects.all(),
|
'queryset': model.objects.all(),
|
||||||
'display_field': display_field,
|
|
||||||
'query_params': query_params,
|
'query_params': query_params,
|
||||||
'null_option': null_option,
|
'null_option': null_option,
|
||||||
})
|
})
|
||||||
|
@ -340,10 +340,10 @@ class IPAddressTable(BaseTable):
|
|||||||
verbose_name='Interface'
|
verbose_name='Interface'
|
||||||
)
|
)
|
||||||
assigned_object_parent = tables.Column(
|
assigned_object_parent = tables.Column(
|
||||||
accessor='assigned_object__parent',
|
accessor='assigned_object.parent_object',
|
||||||
linkify=True,
|
linkify=True,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Interface Parent'
|
verbose_name='Device/VM'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.11.0-dev'
|
VERSION = '2.12-beta1'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -25,15 +25,9 @@ HOSTNAME = platform.node()
|
|||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# Validate Python version
|
# Validate Python version
|
||||||
if platform.python_version_tuple() < ('3', '6'):
|
|
||||||
raise RuntimeError(
|
|
||||||
"NetBox requires Python 3.6 or higher (current: Python {})".format(platform.python_version())
|
|
||||||
)
|
|
||||||
# TODO: Remove in NetBox v2.12
|
|
||||||
if platform.python_version_tuple() < ('3', '7'):
|
if platform.python_version_tuple() < ('3', '7'):
|
||||||
warnings.warn(
|
raise RuntimeError(
|
||||||
"Support for Python 3.6 will be dropped in NetBox v2.12. Please upgrade to Python 3.7 or later at your "
|
f"NetBox requires Python 3.7 or higher (current: Python {platform.python_version()})"
|
||||||
"earliest convenience."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,13 +44,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Racks</th>
|
<th scope="row">Racks</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ object.racks.count }}</a>
|
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Devices</th>
|
<th scope="row">Devices</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
|
<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ device_count }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@ -83,20 +83,20 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Devices
|
Locations
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% include 'inc/table.html' with table=devices_table %}
|
{% include 'inc/table.html' with table=child_locations_table %}
|
||||||
</div>
|
</div>
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_location %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'dcim:device_add' %}?location={{ object.pk }}" class="btn btn-sm btn-primary">
|
<a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Location
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
|
{% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %}
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,7 +92,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include table_template|default:'responsive_table.html' %}
|
<div class="table-responsive">
|
||||||
|
{% render_table table 'inc/table.html' %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
|
@ -328,7 +328,6 @@ class ExpandableIPAddressField(forms.CharField):
|
|||||||
|
|
||||||
class DynamicModelChoiceMixin:
|
class DynamicModelChoiceMixin:
|
||||||
"""
|
"""
|
||||||
:param display_field: The name of the attribute of an API response object to display in the selection list
|
|
||||||
:param query_params: A dictionary of additional key/value pairs to attach to the API request
|
:param query_params: A dictionary of additional key/value pairs to attach to the API request
|
||||||
:param initial_params: A dictionary of child field references to use for selecting a parent field's initial value
|
:param initial_params: A dictionary of child field references to use for selecting a parent field's initial value
|
||||||
:param null_option: The string used to represent a null selection (if any)
|
:param null_option: The string used to represent a null selection (if any)
|
||||||
@ -338,10 +337,8 @@ class DynamicModelChoiceMixin:
|
|||||||
filter = django_filters.ModelChoiceFilter
|
filter = django_filters.ModelChoiceFilter
|
||||||
widget = widgets.APISelect
|
widget = widgets.APISelect
|
||||||
|
|
||||||
# TODO: Remove display_field in v2.12
|
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, *args,
|
||||||
def __init__(self, display_field='display', query_params=None, initial_params=None, null_option=None,
|
**kwargs):
|
||||||
disabled_indicator=None, *args, **kwargs):
|
|
||||||
self.display_field = display_field
|
|
||||||
self.query_params = query_params or {}
|
self.query_params = query_params or {}
|
||||||
self.initial_params = initial_params or {}
|
self.initial_params = initial_params or {}
|
||||||
self.null_option = null_option
|
self.null_option = null_option
|
||||||
@ -354,9 +351,7 @@ class DynamicModelChoiceMixin:
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def widget_attrs(self, widget):
|
def widget_attrs(self, widget):
|
||||||
attrs = {
|
attrs = {}
|
||||||
'display-field': self.display_field,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set value-field attribute if the field specifies to_field_name
|
# Set value-field attribute if the field specifies to_field_name
|
||||||
if self.to_field_name:
|
if self.to_field_name:
|
||||||
|
@ -376,6 +376,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
||||||
('Cluster', ('cluster_group', 'cluster')),
|
('Cluster', ('cluster_group', 'cluster')),
|
||||||
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
||||||
('Resources', ('vcpus', 'memory', 'disk')),
|
('Resources', ('vcpus', 'memory', 'disk')),
|
||||||
('Config Context', ('local_context_data',)),
|
('Config Context', ('local_context_data',)),
|
||||||
@ -666,7 +667,6 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
|||||||
parent = DynamicModelChoiceField(
|
parent = DynamicModelChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
display_field='display_name',
|
|
||||||
query_params={
|
query_params={
|
||||||
'virtualmachine_id': 'virtual_machine',
|
'virtualmachine_id': 'virtual_machine',
|
||||||
}
|
}
|
||||||
@ -702,6 +702,10 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
|||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
field_order = (
|
||||||
|
'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode',
|
||||||
|
'untagged_vlan', 'tagged_vlans', 'tags'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -751,8 +755,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
)
|
)
|
||||||
parent = DynamicModelChoiceField(
|
parent = DynamicModelChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
required=False,
|
required=False
|
||||||
display_field='display_name'
|
|
||||||
)
|
)
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_virtualmachines(apps, schema_editor):
|
||||||
|
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
|
||||||
|
for name in VirtualMachine.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||||
|
VirtualMachine.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0022_vminterface_parent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='virtualmachine',
|
||||||
|
options={'ordering': ('_name', 'pk')},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('name', max_length=100, blank=True, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_virtualmachines,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -226,6 +226,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
@ -296,7 +301,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name', 'pk') # Name may be non-unique
|
ordering = ('_name', 'pk') # Name may be non-unique
|
||||||
unique_together = [
|
unique_together = [
|
||||||
['cluster', 'tenant', 'name']
|
['cluster', 'tenant', 'name']
|
||||||
]
|
]
|
||||||
@ -463,6 +468,10 @@ class VMInterface(PrimaryModel, BaseInterface):
|
|||||||
f"({self.parent.virtual_machine})."
|
f"({self.parent.virtual_machine})."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# An interface cannot be its own parent
|
||||||
|
if self.pk and self.parent_id == self.pk:
|
||||||
|
raise ValidationError({'parent': "An interface cannot be its own parent."})
|
||||||
|
|
||||||
# Validate untagged VLAN
|
# Validate untagged VLAN
|
||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
Django==3.2
|
Django==3.2
|
||||||
django-cacheops==5.1
|
django-cacheops==5.1
|
||||||
django-cors-headers==3.7.0
|
django-cors-headers==3.7.0
|
||||||
django-debug-toolbar==3.2
|
django-debug-toolbar==3.2.1
|
||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
django-mptt==0.12.0
|
django-mptt==0.12.0
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.1.0
|
django-prometheus==2.1.0
|
||||||
django-rq==2.4.1
|
django-rq==2.4.1
|
||||||
django-tables2==2.3.4
|
django-tables2==2.3.4
|
||||||
django-taggit==1.3.0
|
django-taggit==1.4.0
|
||||||
django-timezone-field==4.1.2
|
django-timezone-field==4.1.2
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.20.0
|
||||||
|
Reference in New Issue
Block a user