Compare commits

...

28 Commits

Author SHA1 Message Date
Jeremy Stretch
8e5aa69321 Merge pull request #5062 from netbox-community/develop
Release v2.9.2
2020-08-27 14:13:58 -04:00
Jeremy Stretch
f3e4911c68 Release v2.9.2 2020-08-27 14:03:51 -04:00
Jeremy Stretch
e8e4ff4111 Closes #5056: Add interface and parent columns to IP address list 2020-08-27 13:46:31 -04:00
Jeremy Stretch
523c32b8af Fixes #5061: Allow adding/removing tags when bulk editing virtual machine interfaces 2020-08-27 13:26:41 -04:00
Jeremy Stretch
5cdccb47f4 Fixes #5060: Fix validation when bulk-importing child devices 2020-08-27 11:27:17 -04:00
Jeremy Stretch
fa73bf8e87 Closes #5505: Add tags column to device/VM component list tables 2020-08-27 09:43:20 -04:00
Jeremy Stretch
5fe4e6cc96 Fixes #5058: Correct URL for front rack elevation images when using external storage 2020-08-27 09:26:56 -04:00
Jeremy Stretch
f23900fc8c Fixes #5059: Fix inclusion of checkboxes for interfaces in virtual machine view 2020-08-27 09:22:53 -04:00
Jeremy Stretch
a0790e9119 Changelog for #5002 2020-08-24 15:17:36 -04:00
Jeremy Stretch
236db7d42d Merge pull request #5039 from innovationnorway/5002-available-prefixes-swagger
Use correct serializer for available-prefixes POST response
2020-08-24 15:09:58 -04:00
Jeremy Stretch
5da7590eea Fixes #4988: Fix ordering of rack reservations with identical creation times 2020-08-24 12:04:48 -04:00
Jeremy Stretch
df97eb2f72 Fixes #5045: Allow assignment of interfaces to non-master VC peer LAG during import 2020-08-24 11:33:45 -04:00
Jeremy Stretch
32a0e519ad Fixes #5041: Fix form tabs when assigning an IP to a VM interface 2020-08-24 10:56:23 -04:00
Jeremy Stretch
78d6561e39 Fixes #5040: Limit SLAAC status to IPv6 addresses 2020-08-24 10:51:47 -04:00
Jeremy Stretch
9147823305 Fixes #5042: Fix display of SLAAC label for IP addresses status 2020-08-24 10:47:26 -04:00
Jeremy Stretch
e7cf87be97 Fixes #5035: Fix exception when modifying an IP address assigned to a VM 2020-08-24 10:39:41 -04:00
Jeremy Stretch
6e28490b84 Fixes #5038: Fix validation of primary IPs assigned to virtual machines 2020-08-24 09:41:04 -04:00
Joakim Bakke Hellum
fcc15d2e33 Use correct serializer for available-prefixes POST response
POST `/ipam/prefixes/{id}/available-prefixes/` returns single `Prefix` object, not list of `AvailablePrefix` objects.
2020-08-23 20:49:50 +02:00
Jeremy Stretch
3522eafd2c Post-release version bump 2020-08-22 21:06:06 -04:00
Jeremy Stretch
848cfeb353 Merge pull request #5034 from netbox-community/develop
Release v2.9.1 - 2020-08-22
2020-08-22 21:05:08 -04:00
Jeremy Stretch
35a280eb31 Release v2.9.1 2020-08-22 21:03:51 -04:00
Jeremy Stretch
aedba0e8be Closes #5030: Call out required minimum versions for depdencies in upgrade documentation 2020-08-22 20:53:21 -04:00
Jeremy Stretch
728088f5fa Closes #5033: Support backward compatibility for REMOTE_AUTH_BACKEND 2020-08-22 20:39:46 -04:00
Jeremy Stretch
2116b928b6 Add link to v2.9 release notes 2020-08-21 16:44:13 -04:00
Jeremy Stretch
f37997ac54 Closes #4814: Allow nested LAG interfaces 2020-08-21 13:35:03 -04:00
Jeremy Stretch
ed65603632 Closes #4540: Add IP address status type for SLAAC 2020-08-21 13:17:41 -04:00
Jeremy Stretch
802af06c0f Closes #4991: Add Python and NetBox versions to error page 2020-08-21 12:58:48 -04:00
Jeremy Stretch
e02590ac96 Post-release version bump 2020-08-21 09:56:29 -04:00
25 changed files with 210 additions and 69 deletions

View File

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

View File

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

View File

@@ -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:

View File

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

View File

@@ -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'

View File

@@ -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)

View File

@@ -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()

View 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']},
),
]

View File

@@ -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]:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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')

View File

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

View File

@@ -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'),
) )

View File

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

View File

@@ -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 '',

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 %}

View File

@@ -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,
})) }))

View File

@@ -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()

View File

@@ -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):

View File

@@ -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')