Skip to content

Commit

Permalink
Merge pull request netbox-community#11174 from netbox-community/develop
Browse files Browse the repository at this point in the history
Release v3.3.10
  • Loading branch information
jeremystretch authored Dec 13, 2022
2 parents 85c6067 + 5e32b39 commit fb27803
Show file tree
Hide file tree
Showing 32 changed files with 318 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
placeholder: v3.3.10
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
placeholder: v3.3.10
validations:
required: true
- type: dropdown
Expand Down
8 changes: 8 additions & 0 deletions docs/configuration/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u

---

## LOGOUT_REDIRECT_URL

Default: `'home'`

The view name or URL to which a user is redirected after logging out.

---

## SESSION_COOKIE_NAME

Default: `sessionid`
Expand Down
3 changes: 3 additions & 0 deletions docs/installation/3-netbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
* Builds the documentation locally (for offline use)
* Aggregate static resource files on disk

!!! warning
If you still have a Python virtual environment active from a previous installation step, disable it now by running the `deactivate` command. This will avoid errors on systems where `sudo` has been configured to preserve the user's current environment.

```no-highlight
sudo /opt/netbox/upgrade.sh
```
Expand Down
30 changes: 28 additions & 2 deletions docs/release-notes/version-3.3.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# NetBox v3.3

## v3.3.10 (2022-12-13)

### Enhancements

* [#9361](https://github.com/netbox-community/netbox/issues/9361) - Add replication controls for module bulk import
* [#10255](https://github.com/netbox-community/netbox/issues/10255) - Introduce `LOGOUT_REDIRECT_URL` config parameter to control redirection of user after logout
* [#10447](https://github.com/netbox-community/netbox/issues/10447) - Enable reassigning an inventory item from one device to another
* [#10516](https://github.com/netbox-community/netbox/issues/10516) - Add vertical frame & cabinet rack types
* [#10748](https://github.com/netbox-community/netbox/issues/10748) - Add provider selection field for provider networks to circuit termination edit view
* [#11089](https://github.com/netbox-community/netbox/issues/11089) - Permit whitespace in MAC addresses
* [#11119](https://github.com/netbox-community/netbox/issues/11119) - Enable filtering L2VPNs by slug

### Bug Fixes

* [#11041](https://github.com/netbox-community/netbox/issues/11041) - Correct power utilization percentage precision
* [#11077](https://github.com/netbox-community/netbox/issues/11077) - Honor configured date format when displaying date custom field values in tables
* [#11087](https://github.com/netbox-community/netbox/issues/11087) - Fix background color of bottom banner content
* [#11101](https://github.com/netbox-community/netbox/issues/11101) - Correct circuits count under site view
* [#11109](https://github.com/netbox-community/netbox/issues/11109) - Fix nullification of custom object & multi-object fields via REST API
* [#11128](https://github.com/netbox-community/netbox/issues/11128) - Disable ordering changelog table by object to avoid exception
* [#11142](https://github.com/netbox-community/netbox/issues/11142) - Correct available choices for status under IP range filter form
* [#11168](https://github.com/netbox-community/netbox/issues/11168) - Honor `RQ_DEFAULT_TIMEOUT` config parameter when using Redis Sentinel
* [#11173](https://github.com/netbox-community/netbox/issues/11173) - Enable missing tags columns for contact, L2VPN lists

---

## v3.3.9 (2022-11-30)

### Enhancements
Expand Down Expand Up @@ -452,7 +478,7 @@ Custom field UI visibility has no impact on API operation.
* The `cluster` field is now optional. A virtual machine must have a site and/or cluster assigned.
* Added the optional `device` field
* Added the `l2vpn_termination` read-only field
wireless.WirelessLAN
* wireless.WirelessLAN
* Added `tenant` field
wireless.WirelessLink
* wireless.WirelessLink
* Added `tenant` field
16 changes: 14 additions & 2 deletions netbox/circuits/forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,28 @@ class CircuitTerminationForm(NetBoxModelForm):
},
required=False
)
provider_network_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False,
label='Provider',
initial_params={
'networks': 'provider_network'
}
)
provider_network = DynamicModelChoiceField(
queryset=ProviderNetwork.objects.all(),
query_params={
'provider_id': '$provider_network_provider',
},
required=False
)

class Meta:
model = CircuitTermination
fields = [
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected',
'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network_provider',
'provider_network', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'tags',
]
help_texts = {
'port_speed': "Physical circuit speed",
Expand Down
4 changes: 4 additions & 0 deletions netbox/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,18 @@ class RackTypeChoices(ChoiceSet):
TYPE_4POST = '4-post-frame'
TYPE_CABINET = '4-post-cabinet'
TYPE_WALLFRAME = 'wall-frame'
TYPE_WALLFRAME_VERTICAL = 'wall-frame-vertical'
TYPE_WALLCABINET = 'wall-cabinet'
TYPE_WALLCABINET_VERTICAL = 'wall-cabinet-vertical'

CHOICES = (
(TYPE_2POST, '2-post frame'),
(TYPE_4POST, '4-post frame'),
(TYPE_CABINET, '4-post cabinet'),
(TYPE_WALLFRAME, 'Wall-mounted frame'),
(TYPE_WALLFRAME_VERTICAL, 'Wall-mounted frame (vertical)'),
(TYPE_WALLCABINET, 'Wall-mounted cabinet'),
(TYPE_WALLCABINET_VERTICAL, 'Wall-mounted cabinet (vertical)'),
)


Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def from_db_value(self, value, expression, connection):
def to_python(self, value):
if value is None:
return value
if type(value) is str:
value = value.replace(' ', '')
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError:
Expand Down
21 changes: 19 additions & 2 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
from virtualization.models import Cluster
from wireless.choices import WirelessRoleChoices
from .common import ModuleCommonForm

__all__ = (
'CableCSVForm',
Expand Down Expand Up @@ -407,7 +408,7 @@ def __init__(self, data=None, *args, **kwargs):
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)


class ModuleCSVForm(NetBoxModelCSVForm):
class ModuleCSVForm(ModuleCommonForm, NetBoxModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name'
Expand All @@ -420,11 +421,20 @@ class ModuleCSVForm(NetBoxModelCSVForm):
queryset=ModuleType.objects.all(),
to_field_name='model'
)
replicate_components = forms.BooleanField(
required=False,
help_text="Automatically populate components associated with this module type (default: true)"
)
adopt_components = forms.BooleanField(
required=False,
help_text="Adopt already existing components"
)

class Meta:
model = Module
fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments',
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'replicate_components',
'adopt_components', 'comments',
)

def __init__(self, data=None, *args, **kwargs):
Expand All @@ -435,6 +445,13 @@ def __init__(self, data=None, *args, **kwargs):
params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)

def clean_replicate_components(self):
# Make sure replicate_components is True when it's not included in the uploaded data
if 'replicate_components' not in self.data:
return True
else:
return self.cleaned_data['replicate_components']


class ChildDeviceCSVForm(BaseDeviceCSVForm):
parent = CSVModelChoiceField(
Expand Down
58 changes: 58 additions & 0 deletions netbox/dcim/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

__all__ = (
'InterfaceCommonForm',
'ModuleCommonForm'
)


Expand Down Expand Up @@ -47,3 +48,60 @@ def clean(self):
'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
f"the interface's parent device/VM, or they must be global"
})


class ModuleCommonForm(forms.Form):
def clean(self):
super().clean()

replicate_components = self.cleaned_data.get("replicate_components")
adopt_components = self.cleaned_data.get("adopt_components")
device = self.cleaned_data['device']
module_type = self.cleaned_data['module_type']
module_bay = self.cleaned_data['module_bay']

if adopt_components:
self.instance._adopt_components = True

# Bail out if we are not installing a new module or if we are not replicating components
if self.instance.pk or not replicate_components:
self.instance._disable_replication = True
return

for templates, component_attribute in [
("consoleporttemplates", "consoleports"),
("consoleserverporttemplates", "consoleserverports"),
("interfacetemplates", "interfaces"),
("powerporttemplates", "powerports"),
("poweroutlettemplates", "poweroutlets"),
("rearporttemplates", "rearports"),
("frontporttemplates", "frontports")
]:
# Prefetch installed components
installed_components = {
component.name: component for component in getattr(device, component_attribute).all()
}

# Get the templates for the module type.
for template in getattr(module_type, templates).all():
# Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError(
"Cannot install module with placeholder values in a module bay with no position defined"
)

resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
existing_item = installed_components.get(resolved_name)

# It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError(
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
f"to a module"
)

# If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError(
f"{template.component_model.__name__} - {resolved_name} already exists"
)
73 changes: 9 additions & 64 deletions netbox/dcim/forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)
from virtualization.models import Cluster, ClusterGroup
from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm
from .common import InterfaceCommonForm, ModuleCommonForm

__all__ = (
'CableForm',
Expand Down Expand Up @@ -657,7 +657,7 @@ def __init__(self, *args, **kwargs):
self.fields['position'].widget.choices = [(position, f'U{position}')]


class ModuleForm(NetBoxModelForm):
class ModuleForm(ModuleCommonForm, NetBoxModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
initial_params={
Expand Down Expand Up @@ -722,68 +722,6 @@ def __init__(self, *args, **kwargs):
self.fields['adopt_components'].initial = False
self.fields['adopt_components'].disabled = True

def save(self, *args, **kwargs):

# If replicate_components is False, disable automatic component replication on the instance
if self.instance.pk or not self.cleaned_data['replicate_components']:
self.instance._disable_replication = True

if self.cleaned_data['adopt_components']:
self.instance._adopt_components = True

return super().save(*args, **kwargs)

def clean(self):
super().clean()

replicate_components = self.cleaned_data.get("replicate_components")
adopt_components = self.cleaned_data.get("adopt_components")
device = self.cleaned_data['device']
module_type = self.cleaned_data['module_type']
module_bay = self.cleaned_data['module_bay']

# Bail out if we are not installing a new module or if we are not replicating components
if self.instance.pk or not replicate_components:
return

for templates, component_attribute in [
("consoleporttemplates", "consoleports"),
("consoleserverporttemplates", "consoleserverports"),
("interfacetemplates", "interfaces"),
("powerporttemplates", "powerports"),
("poweroutlettemplates", "poweroutlets"),
("rearporttemplates", "rearports"),
("frontporttemplates", "frontports")
]:
# Prefetch installed components
installed_components = {
component.name: component for component in getattr(device, component_attribute).all()
}

# Get the templates for the module type.
for template in getattr(module_type, templates).all():
# Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError(
"Cannot install module with placeholder values in a module bay with no position defined"
)

resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
existing_item = installed_components.get(resolved_name)

# It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError(
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
f"to a module"
)

# If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError(
f"{template.component_model.__name__} - {resolved_name} already exists"
)


class CableForm(TenancyForm, NetBoxModelForm):

Expand Down Expand Up @@ -1610,6 +1548,13 @@ class InventoryItemForm(DeviceComponentForm):
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Specifically allow editing the device of IntentoryItems
if self.instance.pk:
self.fields['device'].disabled = False

class Meta:
model = InventoryItem
fields = [
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,3 +1153,20 @@ def clean(self):
raise ValidationError({
"parent": "Cannot assign self as parent."
})

# Validation for moving InventoryItems
if self.pk:
# Cannot move an InventoryItem to another device if it has a parent
if self.parent and self.parent.device != self.device:
raise ValidationError({
"parent": "Parent inventory item does not belong to the same device."
})

# Prevent moving InventoryItems with children
first_child = self.get_children().first()
if first_child and first_child.device != self.device:
raise ValidationError("Cannot move an inventory item with dependent children")

# When moving an InventoryItem to another device, remove any associated component
if self.component and self.component.device != self.device:
self.component = None
2 changes: 1 addition & 1 deletion netbox/dcim/tables/devicetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Meta(NetBoxTable.Meta):
model = Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'contacts', 'actions', 'created', 'last_updated',
'tags', 'contacts', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
Expand Down
Loading

0 comments on commit fb27803

Please sign in to comment.