Skip to content

Commit

Permalink
Merge pull request #11376 from netbox-community/develop
Browse files Browse the repository at this point in the history
Release v3.4.2
  • Loading branch information
jeremystretch authored Jan 3, 2023
2 parents 27c71b8 + e940f00 commit 04137e8
Show file tree
Hide file tree
Showing 34 changed files with 403 additions and 59 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.4.1
placeholder: v3.4.2
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.4.1
placeholder: v3.4.2
validations:
required: true
- type: dropdown
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration/required-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes

* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
* `USERNAME` - Redis username (if set)
* `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis
Expand All @@ -75,13 +76,15 @@ REDIS = {
'tasks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'USERNAME': 'netbox'
'PASSWORD': 'foobar',
'DATABASE': 0,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'USERNAME': ''
'PASSWORD': '',
'DATABASE': 1,
'SSL': False,
Expand Down
8 changes: 8 additions & 0 deletions docs/configuration/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ Email is sent from NetBox only for critical events or if configured for [logging

---

## ENABLE_LOCALIZATION

Default: False

Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding what is set for DATE_FORMAT) based on the browser locale as well as translate certain strings from third party modules.

---

## HTTP_PROXIES

Default: None
Expand Down
23 changes: 23 additions & 0 deletions docs/release-notes/version-3.4.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# NetBox v3.4

## v3.4.2 (2023-01-03)

### Enhancements

* [#9285](https://github.com/netbox-community/netbox/issues/9285) - Enable specifying assigned component during bulk import of inventory items
* [#10700](https://github.com/netbox-community/netbox/issues/10700) - Match device name when using modules quick search
* [#11121](https://github.com/netbox-community/netbox/issues/11121) - Add VM resource totals to cluster view
* [#11156](https://github.com/netbox-community/netbox/issues/11156) - Enable selecting assigned component when editing inventory item in UI
* [#11223](https://github.com/netbox-community/netbox/issues/11223) - `reindex` management command should accept app label without model name
* [#11244](https://github.com/netbox-community/netbox/issues/11244) - Add controls for saved filters to rack elevations list
* [#11248](https://github.com/netbox-community/netbox/issues/11248) - Fix database migration when plugin with search indexer is enabled
* [#11259](https://github.com/netbox-community/netbox/issues/11259) - Add support for Redis username configuration

### Bug Fixes

* [#11280](https://github.com/netbox-community/netbox/issues/11280) - Fix errant newlines when exporting interfaces with multiple IP addresses assigned
* [#11290](https://github.com/netbox-community/netbox/issues/11290) - Correct reporting of scheduled job duration
* [#11232](https://github.com/netbox-community/netbox/issues/11232) - Enable partial & regular expression matching for non-string types in global search
* [#11342](https://github.com/netbox-community/netbox/issues/11342) - Correct cable trace URL under "connection" tab for device components
* [#11345](https://github.com/netbox-community/netbox/issues/11345) - Fix form validation for bulk import of modules

---

## v3.4.1 (2022-12-16)

### Enhancements
Expand Down
1 change: 1 addition & 0 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,7 @@ def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value.strip()) |
Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
Expand Down
30 changes: 29 additions & 1 deletion netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,12 +885,22 @@ class InventoryItemImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Parent inventory item')
)
component_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False,
help_text=_('Component Type')
)
component_name = forms.CharField(
required=False,
help_text=_('Component Name')
)

class Meta:
model = InventoryItem
fields = (
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags'
'description', 'tags', 'component_type', 'component_name',
)

def __init__(self, *args, **kwargs):
Expand All @@ -908,6 +918,24 @@ def __init__(self, *args, **kwargs):
else:
self.fields['parent'].queryset = InventoryItem.objects.none()

def clean_component_name(self):
content_type = self.cleaned_data.get('component_type')
component_name = self.cleaned_data.get('component_name')
device = self.cleaned_data.get("device")

if not device and hasattr(self, 'instance'):
device = self.instance.device

if not all([device, content_type, component_name]):
return None

model = content_type.model_class()
try:
component = model.objects.get(device=device, name=component_name)
self.instance.component = component
except ObjectDoesNotExist:
raise forms.ValidationError(f"Component not found: {device} - {component_name}")


#
# Device component roles
Expand Down
9 changes: 5 additions & 4 deletions netbox/dcim/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@ 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")
replicate_components = self.cleaned_data.get('replicate_components')
adopt_components = self.cleaned_data.get('adopt_components')
device = self.cleaned_data.get('device')
module_type = self.cleaned_data.get('module_type')
module_bay = self.cleaned_data.get('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:
# Bail out if we are not installing a new module or if we are not replicating components (or if
# validation has already failed)
if self.errors or self.instance.pk or not replicate_components:
self.instance._disable_replication = True
return

Expand Down
111 changes: 99 additions & 12 deletions netbox/dcim/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,38 +1549,125 @@ class InventoryItemForm(DeviceComponentForm):
queryset=Manufacturer.objects.all(),
required=False
)
component_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,

# Assigned component selectors
consoleport = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
required=False,
widget=forms.HiddenInput
query_params={
'device_id': '$device'
},
label=_('Console port')
)
component_id = forms.IntegerField(
consoleserverport = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
required=False,
widget=forms.HiddenInput
query_params={
'device_id': '$device'
},
label=_('Console server port')
)
frontport = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Front port')
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Interface')
)
poweroutlet = DynamicModelChoiceField(
queryset=PowerOutlet.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power outlet')
)
powerport = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power port')
)
rearport = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Rear port')
)

fieldsets = (
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
)

class Meta:
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
]

def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
component_type = initial.get('component_type')
component_id = initial.get('component_id')

# Used for picking the default active tab for component selection
self.no_component = True

if instance:
# When editing set the initial value for component selectin
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
if type(instance.component) is component_model.model_class():
initial[component_model.model] = instance.component
self.no_component = False
break
elif component_type and component_id:
# When adding the InventoryItem from a component page
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
if component := content_type.model_class().objects.filter(pk=component_id).first():
initial[content_type.model] = component
self.no_component = False

kwargs['initial'] = initial

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 = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'component_type', 'component_id', 'tags',
def clean(self):
super().clean()

# Handle object assignment
selected_objects = [
field for field in (
'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
) if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]]
else:
self.instance.component = None


#
# Device component roles
#

Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,3 +1146,8 @@ def clean(self):
# When moving an InventoryItem to another device, remove any associated component
if self.component and self.component.device != self.device:
self.component = None
else:
if self.component and self.component.device != self.device:
raise ValidationError({
"device": "Cannot assign inventory item to component on another device"
})
2 changes: 1 addition & 1 deletion netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ def get_status_color(self):
def clean(self):
super().clean()

if self.module_bay.device != self.device:
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
)
Expand Down
3 changes: 3 additions & 0 deletions netbox/dcim/tables/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,9 @@ class BaseInterfaceTable(NetBoxTable):
verbose_name='Tagged VLANs'
)

def value_ip_addresses(self, value):
return ",".join([str(obj.address) for obj in value.all()])


class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
device = tables.Column(
Expand Down
14 changes: 3 additions & 11 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ def get(self, request):
'sort_choices': ORDERING_CHOICES,
'rack_face': rack_face,
'filter_form': forms.RackElevationFilterForm(request.GET),
'model': self.queryset.model,
})


Expand Down Expand Up @@ -2913,23 +2914,14 @@ class InventoryItemView(generic.ObjectView):
class InventoryItemEditView(generic.ObjectEditView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'


class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm

def alter_object(self, instance, request):
# Set component (if any)
component_type = request.GET.get('component_type')
component_id = request.GET.get('component_id')

if component_type and component_id:
content_type = get_object_or_404(ContentType, pk=component_type)
instance.component = get_object_or_404(content_type.model_class(), pk=component_id)

return instance
template_name = 'dcim/inventoryitem_edit.html'


@register_model_view(InventoryItem, 'delete')
Expand Down
1 change: 0 additions & 1 deletion netbox/extras/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
label=_('Model(s)')
)
object_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
Expand Down
Loading

0 comments on commit 04137e8

Please sign in to comment.