mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-08 21:02:18 -06:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5aa69321 | ||
|
|
f3e4911c68 | ||
|
|
e8e4ff4111 | ||
|
|
523c32b8af | ||
|
|
5cdccb47f4 | ||
|
|
fa73bf8e87 | ||
|
|
5fe4e6cc96 | ||
|
|
f23900fc8c | ||
|
|
a0790e9119 | ||
|
|
236db7d42d | ||
|
|
5da7590eea | ||
|
|
df97eb2f72 | ||
|
|
32a0e519ad | ||
|
|
78d6561e39 | ||
|
|
9147823305 | ||
|
|
e7cf87be97 | ||
|
|
6e28490b84 | ||
|
|
fcc15d2e33 | ||
|
|
3522eafd2c | ||
|
|
848cfeb353 | ||
|
|
35a280eb31 | ||
|
|
aedba0e8be | ||
|
|
728088f5fa | ||
|
|
2116b928b6 | ||
|
|
f37997ac54 | ||
|
|
ed65603632 | ||
|
|
802af06c0f | ||
|
|
e02590ac96 |
@@ -4,8 +4,15 @@
|
|||||||
|
|
||||||
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the version in which the change went into effect.
|
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the version in which the change went into effect.
|
||||||
|
|
||||||
!!! note
|
## Update Dependencies to Required Versions
|
||||||
Beginning with version 2.8, NetBox requires Python 3.6 or later.
|
|
||||||
|
NetBox v2.9.0 and later requires the following:
|
||||||
|
|
||||||
|
| Dependency | Minimum Version |
|
||||||
|
|------------|-----------------|
|
||||||
|
| Python | 3.6 |
|
||||||
|
| PostgreSQL | 9.6 |
|
||||||
|
| Redis | 4.0 |
|
||||||
|
|
||||||
## Install the Latest Code
|
## Install the Latest Code
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Interfaces in NetBox represent network interfaces used to exchange data with con
|
|||||||
|
|
||||||
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces.
|
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces.
|
||||||
|
|
||||||
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
||||||
|
|
||||||
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Each IP address can also be assigned an operational status and a functional role
|
|||||||
* Reserved
|
* Reserved
|
||||||
* Deprecated
|
* Deprecated
|
||||||
* DHCP
|
* DHCP
|
||||||
|
* SLAAC (IPv6 Stateless Address Autoconfiguration)
|
||||||
|
|
||||||
Roles are used to indicate some special attribute of an IP address; for example, use as a loopback or as the the virtual IP for a VRRP group. (Note that functional roles are conceptual in nature, and thus cannot be customized by the user.) Available roles include:
|
Roles are used to indicate some special attribute of an IP address; for example, use as a loopback or as the the virtual IP for a VRRP group. (Note that functional roles are conceptual in nature, and thus cannot be customized by the user.) Available roles include:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,44 @@
|
|||||||
# NetBox v2.9
|
# NetBox v2.9
|
||||||
|
|
||||||
|
## v2.9.2 (2020-08-27)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5055](https://github.com/netbox-community/netbox/issues/5055) - Add tags column to device/VM component list tables
|
||||||
|
* [#5056](https://github.com/netbox-community/netbox/issues/5056) - Add interface and parent columns to IP address list
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4988](https://github.com/netbox-community/netbox/issues/4988) - Fix ordering of rack reservations with identical creation times
|
||||||
|
* [#5002](https://github.com/netbox-community/netbox/issues/5002) - Correct OpenAPI definition for `available-prefixes` endpoint
|
||||||
|
* [#5035](https://github.com/netbox-community/netbox/issues/5035) - Fix exception when modifying an IP address assigned to a VM
|
||||||
|
* [#5038](https://github.com/netbox-community/netbox/issues/5038) - Fix validation of primary IPs assigned to virtual machines
|
||||||
|
* [#5040](https://github.com/netbox-community/netbox/issues/5040) - Limit SLAAC status to IPv6 addresses
|
||||||
|
* [#5041](https://github.com/netbox-community/netbox/issues/5041) - Fix form tabs when assigning an IP to a VM interface
|
||||||
|
* [#5042](https://github.com/netbox-community/netbox/issues/5042) - Fix display of SLAAC label for IP addresses status
|
||||||
|
* [#5045](https://github.com/netbox-community/netbox/issues/5045) - Allow assignment of interfaces to non-master VC peer LAG during import
|
||||||
|
* [#5058](https://github.com/netbox-community/netbox/issues/5058) - Correct URL for front rack elevation images when using external storage
|
||||||
|
* [#5059](https://github.com/netbox-community/netbox/issues/5059) - Fix inclusion of checkboxes for interfaces in virtual machine view
|
||||||
|
* [#5060](https://github.com/netbox-community/netbox/issues/5060) - Fix validation when bulk-importing child devices
|
||||||
|
* [#5061](https://github.com/netbox-community/netbox/issues/5061) - Allow adding/removing tags when bulk editing virtual machine interfaces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.1 (2020-08-22)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4540](https://github.com/netbox-community/netbox/issues/4540) - Add IP address status type for SLAAC
|
||||||
|
* [#4814](https://github.com/netbox-community/netbox/issues/4814) - Allow nested LAG interfaces
|
||||||
|
* [#4991](https://github.com/netbox-community/netbox/issues/4991) - Add Python and NetBox versions to error page
|
||||||
|
* [#5033](https://github.com/netbox-community/netbox/issues/5033) - Support backward compatibility for `REMOTE_AUTH_BACKEND` configuration parameter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.9.0 (2020-08-21)
|
## v2.9.0 (2020-08-21)
|
||||||
|
|
||||||
|
**Note:** Redis 4.0 or later is required for this release.
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
#### Object-Based Permissions ([#554](https://github.com/netbox-community/netbox/issues/554))
|
#### Object-Based Permissions ([#554](https://github.com/netbox-community/netbox/issues/554))
|
||||||
@@ -56,7 +93,8 @@ Two new REST API endpoints have been added to facilitate the retrieval and manip
|
|||||||
|
|
||||||
### Configuration Changes
|
### Configuration Changes
|
||||||
|
|
||||||
* If in use, LDAP authentication must be enabled by setting `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
* If using NetBox's built-in remote authentication backend, update `REMOTE_AUTH_BACKEND` to `'netbox.authentication.RemoteUserBackend'`, as the authentication class has moved.
|
||||||
|
* If using LDAP authentication, set `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
||||||
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ nav:
|
|||||||
- User Preferences: 'development/user-preferences.md'
|
- User Preferences: 'development/user-preferences.md'
|
||||||
- Release Checklist: 'development/release-checklist.md'
|
- Release Checklist: 'development/release-checklist.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
|
- Version 2.9: 'release-notes/version-2.9.md'
|
||||||
- Version 2.8: 'release-notes/version-2.8.md'
|
- Version 2.8: 'release-notes/version-2.8.md'
|
||||||
- Version 2.7: 'release-notes/version-2.7.md'
|
- Version 2.7: 'release-notes/version-2.7.md'
|
||||||
- Version 2.6: 'release-notes/version-2.6.md'
|
- Version 2.6: 'release-notes/version-2.6.md'
|
||||||
|
|||||||
@@ -94,8 +94,12 @@ class RackElevationSVG:
|
|||||||
|
|
||||||
# Embed front device type image if one exists
|
# Embed front device type image if one exists
|
||||||
if self.include_images and device.device_type.front_image:
|
if self.include_images and device.device_type.front_image:
|
||||||
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
|
image = drawing.image(
|
||||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
href=device.device_type.front_image.url,
|
||||||
|
insert=start,
|
||||||
|
size=end,
|
||||||
|
class_='device-image'
|
||||||
|
)
|
||||||
image.fit(scale='slice')
|
image.fit(scale='slice')
|
||||||
link.add(image)
|
link.add(image)
|
||||||
|
|
||||||
@@ -107,8 +111,12 @@ class RackElevationSVG:
|
|||||||
|
|
||||||
# Embed rear device type image if one exists
|
# Embed rear device type image if one exists
|
||||||
if self.include_images and device.device_type.rear_image:
|
if self.include_images and device.device_type.rear_image:
|
||||||
url = device.device_type.rear_image.url
|
image = drawing.image(
|
||||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
href=device.device_type.rear_image.url,
|
||||||
|
insert=start,
|
||||||
|
size=end,
|
||||||
|
class_='device-image'
|
||||||
|
)
|
||||||
image.fit(scale='slice')
|
image.fit(scale='slice')
|
||||||
drawing.add(image)
|
drawing.add(image)
|
||||||
|
|
||||||
|
|||||||
@@ -1811,7 +1811,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
nat_inside__assigned_object_id__in=interface_ids
|
nat_inside__assigned_object_id__in=interface_ids
|
||||||
).prefetch_related('assigned_object')
|
).prefetch_related('assigned_object')
|
||||||
if nat_ips:
|
if nat_ips:
|
||||||
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in nat_ips]
|
ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
|
||||||
ip_choices.append(('NAT IPs', ip_list))
|
ip_choices.append(('NAT IPs', ip_list))
|
||||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||||
|
|
||||||
@@ -2686,7 +2686,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
|||||||
device_query = Q(device=device)
|
device_query = Q(device=device)
|
||||||
if device.virtual_chassis:
|
if device.virtual_chassis:
|
||||||
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
|
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(device_query, type=InterfaceTypeChoices.TYPE_LAG)
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
|
device_query,
|
||||||
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
|
).exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
||||||
@@ -2876,17 +2879,22 @@ class InterfaceCSVForm(CSVModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device (or VC master)
|
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
|
||||||
device = None
|
device = None
|
||||||
if self.is_bound and 'device' in self.data:
|
if self.is_bound and 'device' in self.data:
|
||||||
try:
|
try:
|
||||||
device = self.fields['device'].to_python(self.data['device'])
|
device = self.fields['device'].to_python(self.data['device'])
|
||||||
except forms.ValidationError:
|
except forms.ValidationError:
|
||||||
pass
|
pass
|
||||||
|
if device and device.virtual_chassis:
|
||||||
if device:
|
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG
|
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
|
||||||
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
|
)
|
||||||
|
elif device:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
|
device=device,
|
||||||
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fields['lag'].queryset = Interface.objects.none()
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|||||||
17
netbox/dcim/migrations/0115_rackreservation_order.py
Normal file
17
netbox/dcim/migrations/0115_rackreservation_order.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.1 on 2020-08-24 16:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0114_update_jsonfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rackreservation',
|
||||||
|
options={'ordering': ['created', 'pk']},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -702,18 +702,12 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# A virtual interface cannot have a parent LAG
|
# A virtual interface cannot have a parent LAG
|
||||||
if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
|
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
|
||||||
raise ValidationError({
|
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
|
||||||
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display())
|
|
||||||
})
|
|
||||||
|
|
||||||
# Only a LAG can have LAG members
|
# A LAG interface cannot be its own parent
|
||||||
if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists():
|
if self.pk and self.lag_id == self.pk:
|
||||||
raise ValidationError({
|
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||||
'type': "Cannot change interface type; it has LAG members ({}).".format(
|
|
||||||
", ".join([iface.name for iface in self.member_interfaces.all()])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Validate untagged VLAN
|
# Validate untagged VLAN
|
||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
||||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||||
# of the uniqueness constraint without manual intervention.
|
# of the uniqueness constraint without manual intervention.
|
||||||
if self.name and self.tenant is None:
|
if self.name and hasattr(self, 'site') and self.tenant is None:
|
||||||
if Device.objects.exclude(pk=self.pk).filter(
|
if Device.objects.exclude(pk=self.pk).filter(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
site=self.site,
|
site=self.site,
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ class RackReservation(ChangeLoggedModel):
|
|||||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['created']
|
ordering = ['created', 'pk']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Reservation for rack {}".format(self.rack)
|
return "Reservation for rack {}".format(self.rack)
|
||||||
|
|||||||
@@ -706,34 +706,48 @@ class DeviceComponentTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortTable(DeviceComponentTable):
|
class ConsolePortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:consoleport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTable(DeviceComponentTable):
|
class ConsoleServerPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:consoleserverport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTable(DeviceComponentTable):
|
class PowerPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:powerport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
|
fields = (
|
||||||
|
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable', 'tags',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTable(DeviceComponentTable):
|
class PowerOutletTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:poweroutlet_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -753,12 +767,15 @@ class BaseInterfaceTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:interface_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||||
'description', 'cable', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
'description', 'cable', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@@ -767,18 +784,26 @@ class FrontPortTable(DeviceComponentTable):
|
|||||||
rear_port_position = tables.Column(
|
rear_port_position = tables.Column(
|
||||||
verbose_name='Position'
|
verbose_name='Position'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:frontport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
|
fields = (
|
||||||
|
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||||
|
|
||||||
|
|
||||||
class RearPortTable(DeviceComponentTable):
|
class RearPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:rearport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -786,10 +811,13 @@ class DeviceBayTable(DeviceComponentTable):
|
|||||||
installed_device = tables.Column(
|
installed_device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:devicebay_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -798,12 +826,16 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
discovered = BooleanColumn()
|
discovered = BooleanColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:inventoryitem_list'
|
||||||
|
)
|
||||||
|
cable = None # Override DeviceComponentTable
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||||
'discovered',
|
'discovered', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
@swagger_auto_schema(method='post', responses={201: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='post', responses={201: serializers.PrefixSerializer(many=False)})
|
||||||
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
||||||
def available_prefixes(self, request, pk=None):
|
def available_prefixes(self, request, pk=None):
|
||||||
@@ -247,7 +247,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
filterset_class = filters.IPAddressFilterSet
|
filterset_class = filters.IPAddressFilterSet
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ class IPAddressStatusChoices(ChoiceSet):
|
|||||||
STATUS_RESERVED = 'reserved'
|
STATUS_RESERVED = 'reserved'
|
||||||
STATUS_DEPRECATED = 'deprecated'
|
STATUS_DEPRECATED = 'deprecated'
|
||||||
STATUS_DHCP = 'dhcp'
|
STATUS_DHCP = 'dhcp'
|
||||||
|
STATUS_SLAAC = 'slaac'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(STATUS_ACTIVE, 'Active'),
|
(STATUS_ACTIVE, 'Active'),
|
||||||
(STATUS_RESERVED, 'Reserved'),
|
(STATUS_RESERVED, 'Reserved'),
|
||||||
(STATUS_DEPRECATED, 'Deprecated'),
|
(STATUS_DEPRECATED, 'Deprecated'),
|
||||||
(STATUS_DHCP, 'DHCP'),
|
(STATUS_DHCP, 'DHCP'),
|
||||||
|
(STATUS_SLAAC, 'SLAAC'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
'reserved': 'info',
|
'reserved': 'info',
|
||||||
'deprecated': 'danger',
|
'deprecated': 'danger',
|
||||||
'dhcp': 'success',
|
'dhcp': 'success',
|
||||||
|
'slaac': 'success',
|
||||||
}
|
}
|
||||||
|
|
||||||
ROLE_CLASS_MAP = {
|
ROLE_CLASS_MAP = {
|
||||||
@@ -745,12 +746,18 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
||||||
f"interface"
|
f"interface"
|
||||||
})
|
})
|
||||||
elif self.interface.virtual_machine != vm:
|
elif self.assigned_object.virtual_machine != vm:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
||||||
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Validate IP status selection
|
||||||
|
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
||||||
|
raise ValidationError({
|
||||||
|
'status': "Only IPv6 addresses can be assigned SLAAC status"
|
||||||
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Force dns_name to lowercase
|
# Force dns_name to lowercase
|
||||||
|
|||||||
@@ -387,15 +387,23 @@ class IPAddressTable(BaseTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=TENANT_LINK
|
template_code=TENANT_LINK
|
||||||
)
|
)
|
||||||
assigned = tables.BooleanColumn(
|
assigned_object = tables.Column(
|
||||||
accessor='assigned_object_id',
|
linkify=True,
|
||||||
verbose_name='Assigned'
|
orderable=False,
|
||||||
|
verbose_name='Interface'
|
||||||
|
)
|
||||||
|
assigned_object_parent = tables.Column(
|
||||||
|
accessor='assigned_object__parent',
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Interface Parent'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'assigned_object_parent', 'dns_name',
|
||||||
|
'description',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
|||||||
|
|
||||||
class IPAddressListView(ObjectListView):
|
class IPAddressListView(ObjectListView):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside'
|
'vrf__tenant', 'tenant', 'nat_inside', 'assigned_object'
|
||||||
)
|
)
|
||||||
filterset = filters.IPAddressFilterSet
|
filterset = filters.IPAddressFilterSet
|
||||||
filterset_form = forms.IPAddressFilterForm
|
filterset_form = forms.IPAddressFilterForm
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.9.0'
|
VERSION = '2.9.2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@@ -142,6 +142,13 @@ if type(REMOTE_AUTH_DEFAULT_PERMISSIONS) is not dict:
|
|||||||
)
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise ImproperlyConfigured("REMOTE_AUTH_DEFAULT_PERMISSIONS must be a dictionary.")
|
raise ImproperlyConfigured("REMOTE_AUTH_DEFAULT_PERMISSIONS must be a dictionary.")
|
||||||
|
# Backward compatibility for REMOTE_AUTH_BACKEND
|
||||||
|
if REMOTE_AUTH_BACKEND == 'utilities.auth_backends.RemoteUserBackend':
|
||||||
|
warnings.warn(
|
||||||
|
"RemoteUserBackend has moved! Please update your configuration to:\n"
|
||||||
|
" REMOTE_AUTH_BACKEND='netbox.authentication.RemoteUserBackend'"
|
||||||
|
)
|
||||||
|
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
The complete exception is provided below:
|
The complete exception is provided below:
|
||||||
</p>
|
</p>
|
||||||
<pre><strong>{{ exception }}</strong><br />
|
<pre><strong>{{ exception }}</strong><br />
|
||||||
{{ error }}</pre>
|
{{ error }}
|
||||||
|
|
||||||
|
Python version: {{ python_version }}
|
||||||
|
NetBox version: {{ netbox_version }}</pre>
|
||||||
<p>
|
<p>
|
||||||
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
|
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
|
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
|
||||||
</li>
|
</li>
|
||||||
{% if 'interface' in request.GET %}
|
{% if 'interface' in request.GET or 'vminterface' in request.GET %}
|
||||||
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
|
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.virtualization.change_interface %}
|
{% if perms.virtualization.change_vminterface %}
|
||||||
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.virtualization.delete_interface %}
|
{% if perms.virtualization.delete_vminterface %}
|
||||||
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
{% if ipaddresses %}
|
{% if ipaddresses %}
|
||||||
<tr class="ipaddresses">
|
<tr class="ipaddresses">
|
||||||
{# Placeholder #}
|
{# Placeholder #}
|
||||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@@ -1421,6 +1423,8 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
|
|||||||
type_, error, traceback = sys.exc_info()
|
type_, error, traceback = sys.exc_info()
|
||||||
|
|
||||||
return HttpResponseServerError(template.render({
|
return HttpResponseServerError(template.render({
|
||||||
|
'python_version': platform.python_version(),
|
||||||
|
'netbox_version': settings.VERSION,
|
||||||
'exception': str(type_),
|
'exception': str(type_),
|
||||||
'error': error,
|
'error': error,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
@@ -325,28 +326,28 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||||
for family in [4, 6]:
|
for family in [4, 6]:
|
||||||
ip_choices = [(None, '---------')]
|
ip_choices = [(None, '---------')]
|
||||||
|
|
||||||
|
# Gather PKs of all interfaces belonging to this VM
|
||||||
|
interface_ids = self.instance.interfaces.values_list('pk', flat=True)
|
||||||
|
|
||||||
# Collect interface IPs
|
# Collect interface IPs
|
||||||
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
|
interface_ips = IPAddress.objects.filter(
|
||||||
address__family=family,
|
address__family=family,
|
||||||
vminterface__in=self.instance.interfaces.values_list('id', flat=True)
|
assigned_object_type=ContentType.objects.get_for_model(VMInterface),
|
||||||
|
assigned_object_id__in=interface_ids
|
||||||
)
|
)
|
||||||
if interface_ips:
|
if interface_ips:
|
||||||
ip_choices.append(
|
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
|
||||||
('Interface IPs', [
|
ip_choices.append(('Interface IPs', ip_list))
|
||||||
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
|
|
||||||
])
|
|
||||||
)
|
|
||||||
# Collect NAT IPs
|
# Collect NAT IPs
|
||||||
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
|
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
|
||||||
address__family=family,
|
address__family=family,
|
||||||
nat_inside__vminterface__in=self.instance.interfaces.values_list('id', flat=True)
|
nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface),
|
||||||
|
nat_inside__assigned_object_id__in=interface_ids
|
||||||
)
|
)
|
||||||
if nat_ips:
|
if nat_ips:
|
||||||
ip_choices.append(
|
ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
|
||||||
('NAT IPs', [
|
ip_choices.append(('NAT IPs', ip_list))
|
||||||
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
|
|
||||||
])
|
|
||||||
)
|
|
||||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -683,7 +684,7 @@ class VMInterfaceCSVForm(CSVModelForm):
|
|||||||
return self.cleaned_data['enabled']
|
return self.cleaned_data['enabled']
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
|
|||||||
@@ -335,13 +335,13 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
for field in ['primary_ip4', 'primary_ip6']:
|
for field in ['primary_ip4', 'primary_ip6']:
|
||||||
ip = getattr(self, field)
|
ip = getattr(self, field)
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
if ip.interface in interfaces:
|
if ip.assigned_object in interfaces:
|
||||||
pass
|
pass
|
||||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
|
elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
|
field: f"The specified IP address ({ip}) is not assigned to this VM.",
|
||||||
})
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
|||||||
@@ -154,11 +154,14 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='virtualization:vminterface_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'ip_addresses',
|
'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'tags', 'ip_addresses',
|
||||||
'untagged_vlan', 'tagged_vlans',
|
'untagged_vlan', 'tagged_vlans',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description')
|
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description')
|
||||||
|
|||||||
Reference in New Issue
Block a user