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.
!!! note
Beginning with version 2.8, NetBox requires Python 3.6 or later.
## Update Dependencies to Required Versions
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

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

View File

@@ -10,6 +10,7 @@ Each IP address can also be assigned an operational status and a functional role
* Reserved
* Deprecated
* 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:

View File

@@ -1,7 +1,44 @@
# 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)
**Note:** Redis 4.0 or later is required for this release.
### New Features
#### 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
* 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}`.
### REST API Changes

View File

@@ -76,6 +76,7 @@ nav:
- User Preferences: 'development/user-preferences.md'
- Release Checklist: 'development/release-checklist.md'
- Release Notes:
- Version 2.9: 'release-notes/version-2.9.md'
- Version 2.8: 'release-notes/version-2.8.md'
- Version 2.7: 'release-notes/version-2.7.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
if self.include_images and device.device_type.front_image:
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image = drawing.image(
href=device.device_type.front_image.url,
insert=start,
size=end,
class_='device-image'
)
image.fit(scale='slice')
link.add(image)
@@ -107,8 +111,12 @@ class RackElevationSVG:
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
url = device.device_type.rear_image.url
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image = drawing.image(
href=device.device_type.rear_image.url,
insert=start,
size=end,
class_='device-image'
)
image.fit(scale='slice')
drawing.add(image)

View File

@@ -1811,7 +1811,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
nat_inside__assigned_object_id__in=interface_ids
).prefetch_related('assigned_object')
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))
self.fields['primary_ip{}'.format(family)].choices = ip_choices
@@ -2686,7 +2686,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
device_query = Q(device=device)
if 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
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):
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
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
pass
if device:
if device and device.virtual_chassis:
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:
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
if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
raise ValidationError({
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display())
})
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
# Only a LAG can have LAG members
if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists():
raise ValidationError({
'type': "Cannot change interface type; it has LAG members ({}).".format(
", ".join([iface.name for iface in self.member_interfaces.all()])
)
})
# A LAG interface cannot be its own parent
if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
# Validate untagged VLAN
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
# 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.
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(
name=self.name,
site=self.site,

View File

@@ -600,7 +600,7 @@ class RackReservation(ChangeLoggedModel):
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
class Meta:
ordering = ['created']
ordering = ['created', 'pk']
def __str__(self):
return "Reservation for rack {}".format(self.rack)

View File

@@ -706,34 +706,48 @@ class DeviceComponentTable(BaseTable):
class ConsolePortTable(DeviceComponentTable):
tags = TagColumn(
url_name='dcim:consoleport_list'
)
class Meta(DeviceComponentTable.Meta):
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')
class ConsoleServerPortTable(DeviceComponentTable):
tags = TagColumn(
url_name='dcim:consoleserverport_list'
)
class Meta(DeviceComponentTable.Meta):
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')
class PowerPortTable(DeviceComponentTable):
tags = TagColumn(
url_name='dcim:powerport_list'
)
class Meta(DeviceComponentTable.Meta):
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')
class PowerOutletTable(DeviceComponentTable):
tags = TagColumn(
url_name='dcim:poweroutlet_list'
)
class Meta(DeviceComponentTable.Meta):
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')
@@ -753,12 +767,15 @@ class BaseInterfaceTable(BaseTable):
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
tags = TagColumn(
url_name='dcim:interface_list'
)
class Meta(DeviceComponentTable.Meta):
model = Interface
fields = (
'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')
@@ -767,18 +784,26 @@ class FrontPortTable(DeviceComponentTable):
rear_port_position = tables.Column(
verbose_name='Position'
)
tags = TagColumn(
url_name='dcim:frontport_list'
)
class Meta(DeviceComponentTable.Meta):
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')
class RearPortTable(DeviceComponentTable):
tags = TagColumn(
url_name='dcim:rearport_list'
)
class Meta(DeviceComponentTable.Meta):
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')
@@ -786,10 +811,13 @@ class DeviceBayTable(DeviceComponentTable):
installed_device = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='dcim:devicebay_list'
)
class Meta(DeviceComponentTable.Meta):
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')
@@ -798,12 +826,16 @@ class InventoryItemTable(DeviceComponentTable):
linkify=True
)
discovered = BooleanColumn()
tags = TagColumn(
url_name='dcim:inventoryitem_list'
)
cable = None # Override DeviceComponentTable
class Meta(DeviceComponentTable.Meta):
model = InventoryItem
fields = (
'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')

View File

@@ -88,7 +88,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
return super().get_serializer_class()
@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'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
def available_prefixes(self, request, pk=None):
@@ -247,7 +247,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
class IPAddressViewSet(CustomFieldModelViewSet):
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
filterset_class = filters.IPAddressFilterSet

View File

@@ -41,12 +41,14 @@ class IPAddressStatusChoices(ChoiceSet):
STATUS_RESERVED = 'reserved'
STATUS_DEPRECATED = 'deprecated'
STATUS_DHCP = 'dhcp'
STATUS_SLAAC = 'slaac'
CHOICES = (
(STATUS_ACTIVE, 'Active'),
(STATUS_RESERVED, 'Reserved'),
(STATUS_DEPRECATED, 'Deprecated'),
(STATUS_DHCP, 'DHCP'),
(STATUS_SLAAC, 'SLAAC'),
)

View File

@@ -669,6 +669,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
'reserved': 'info',
'deprecated': 'danger',
'dhcp': 'success',
'slaac': 'success',
}
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 "
f"interface"
})
elif self.interface.virtual_machine != vm:
elif self.assigned_object.virtual_machine != vm:
raise ValidationError({
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
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):
# Force dns_name to lowercase

View File

@@ -387,15 +387,23 @@ class IPAddressTable(BaseTable):
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
)
assigned = tables.BooleanColumn(
accessor='assigned_object_id',
verbose_name='Assigned'
assigned_object = tables.Column(
linkify=True,
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):
model = IPAddress
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 = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',

View File

@@ -493,7 +493,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside'
'vrf__tenant', 'tenant', 'nat_inside', 'assigned_object'
)
filterset = filters.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm

View File

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
VERSION = '2.9.0'
VERSION = '2.9.2'
# Hostname
HOSTNAME = platform.node()
@@ -142,6 +142,13 @@ if type(REMOTE_AUTH_DEFAULT_PERMISSIONS) is not dict:
)
except TypeError:
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:
</p>
<pre><strong>{{ exception }}</strong><br />
{{ error }}</pre>
{{ error }}
Python version: {{ python_version }}
NetBox version: {{ netbox_version }}</pre>
<p>
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
</p>

View File

@@ -4,7 +4,7 @@
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
</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 %}>
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
</li>

View File

@@ -2,7 +2,7 @@
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
{# Checkbox #}
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td>
@@ -48,12 +48,12 @@
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a>
{% 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">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% 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">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
@@ -65,7 +65,7 @@
{% if ipaddresses %}
<tr class="ipaddresses">
{# Placeholder #}
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
<td></td>
{% endif %}

View File

@@ -1,8 +1,10 @@
import logging
import platform
import re
import sys
from copy import deepcopy
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
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()
return HttpResponseServerError(template.render({
'python_version': platform.python_version(),
'netbox_version': settings.VERSION,
'exception': str(type_),
'error': error,
}))

View File

@@ -1,4 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from dcim.choices import InterfaceModeChoices
@@ -325,28 +326,28 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
# Compile list of choices for primary IPv4 and IPv6 addresses
for family in [4, 6]:
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
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
interface_ips = IPAddress.objects.filter(
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:
ip_choices.append(
('Interface IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
])
)
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
ip_choices.append(('Interface IPs', ip_list))
# Collect NAT IPs
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
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:
ip_choices.append(
('NAT IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) 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))
self.fields['primary_ip{}'.format(family)].choices = ip_choices
else:
@@ -683,7 +684,7 @@ class VMInterfaceCSVForm(CSVModelForm):
return self.cleaned_data['enabled']
class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=VMInterface.objects.all(),
widget=forms.MultipleHiddenInput()

View File

@@ -335,13 +335,13 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
for field in ['primary_ip4', 'primary_ip6']:
ip = getattr(self, field)
if ip is not None:
if ip.interface in interfaces:
if ip.assigned_object in interfaces:
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
else:
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):

View File

@@ -154,11 +154,14 @@ class VMInterfaceTable(BaseInterfaceTable):
name = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='virtualization:vminterface_list'
)
class Meta(BaseTable.Meta):
model = VMInterface
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',
)
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description')