mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.4.1
|
placeholder: v3.4.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.4.1
|
placeholder: v3.4.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -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)
|
* `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)
|
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
||||||
|
* `USERNAME` - Redis username (if set)
|
||||||
* `PASSWORD` - Redis password (if set)
|
* `PASSWORD` - Redis password (if set)
|
||||||
* `DATABASE` - Numeric database ID
|
* `DATABASE` - Numeric database ID
|
||||||
* `SSL` - Use SSL connection to Redis
|
* `SSL` - Use SSL connection to Redis
|
||||||
@ -75,6 +76,7 @@ REDIS = {
|
|||||||
'tasks': {
|
'tasks': {
|
||||||
'HOST': 'redis.example.com',
|
'HOST': 'redis.example.com',
|
||||||
'PORT': 1234,
|
'PORT': 1234,
|
||||||
|
'USERNAME': 'netbox'
|
||||||
'PASSWORD': 'foobar',
|
'PASSWORD': 'foobar',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
@ -82,6 +84,7 @@ REDIS = {
|
|||||||
'caching': {
|
'caching': {
|
||||||
'HOST': 'localhost',
|
'HOST': 'localhost',
|
||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
|
'USERNAME': ''
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
|
@ -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
|
## HTTP_PROXIES
|
||||||
|
|
||||||
Default: None
|
Default: None
|
||||||
|
@ -1,5 +1,28 @@
|
|||||||
# NetBox v3.4
|
# 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)
|
## v3.4.1 (2022-12-16)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -1098,6 +1098,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
|
Q(device__name__icontains=value.strip()) |
|
||||||
Q(serial__icontains=value.strip()) |
|
Q(serial__icontains=value.strip()) |
|
||||||
Q(asset_tag__icontains=value.strip()) |
|
Q(asset_tag__icontains=value.strip()) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
|
@ -885,12 +885,22 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('Parent inventory item')
|
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:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
'description', 'tags'
|
'description', 'tags', 'component_type', 'component_name',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -908,6 +918,24 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
else:
|
else:
|
||||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
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
|
# Device component roles
|
||||||
|
@ -56,8 +56,8 @@ class ModuleCommonForm(forms.Form):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
replicate_components = self.cleaned_data.get("replicate_components")
|
replicate_components = self.cleaned_data.get('replicate_components')
|
||||||
adopt_components = self.cleaned_data.get("adopt_components")
|
adopt_components = self.cleaned_data.get('adopt_components')
|
||||||
device = self.cleaned_data.get('device')
|
device = self.cleaned_data.get('device')
|
||||||
module_type = self.cleaned_data.get('module_type')
|
module_type = self.cleaned_data.get('module_type')
|
||||||
module_bay = self.cleaned_data.get('module_bay')
|
module_bay = self.cleaned_data.get('module_bay')
|
||||||
@ -65,8 +65,9 @@ class ModuleCommonForm(forms.Form):
|
|||||||
if adopt_components:
|
if adopt_components:
|
||||||
self.instance._adopt_components = True
|
self.instance._adopt_components = True
|
||||||
|
|
||||||
# Bail out if we are not installing a new module or if we are not replicating components
|
# Bail out if we are not installing a new module or if we are not replicating components (or if
|
||||||
if self.instance.pk or not replicate_components:
|
# validation has already failed)
|
||||||
|
if self.errors or self.instance.pk or not replicate_components:
|
||||||
self.instance._disable_replication = True
|
self.instance._disable_replication = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1549,15 +1549,63 @@ class InventoryItemForm(DeviceComponentForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
component_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
# Assigned component selectors
|
||||||
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
consoleport = DynamicModelChoiceField(
|
||||||
|
queryset=ConsolePort.objects.all(),
|
||||||
required=False,
|
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,
|
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 = (
|
fieldsets = (
|
||||||
@ -1565,22 +1613,61 @@ class InventoryItemForm(DeviceComponentForm):
|
|||||||
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
|
('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):
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Specifically allow editing the device of IntentoryItems
|
# Specifically allow editing the device of IntentoryItems
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
self.fields['device'].disabled = False
|
self.fields['device'].disabled = False
|
||||||
|
|
||||||
class Meta:
|
def clean(self):
|
||||||
model = InventoryItem
|
super().clean()
|
||||||
fields = [
|
|
||||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
# Handle object assignment
|
||||||
'description', 'component_type', 'component_id', 'tags',
|
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
|
# Device component roles
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@ -1146,3 +1146,8 @@ class InventoryItem(MPTTModel, ComponentModel):
|
|||||||
# When moving an InventoryItem to another device, remove any associated component
|
# When moving an InventoryItem to another device, remove any associated component
|
||||||
if self.component and self.component.device != self.device:
|
if self.component and self.component.device != self.device:
|
||||||
self.component = None
|
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"
|
||||||
|
})
|
||||||
|
@ -961,7 +961,7 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.module_bay.device != self.device:
|
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
|
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
|
||||||
)
|
)
|
||||||
|
@ -506,6 +506,9 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
verbose_name='Tagged VLANs'
|
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):
|
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
|
@ -691,6 +691,7 @@ class RackElevationListView(generic.ObjectListView):
|
|||||||
'sort_choices': ORDERING_CHOICES,
|
'sort_choices': ORDERING_CHOICES,
|
||||||
'rack_face': rack_face,
|
'rack_face': rack_face,
|
||||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||||
|
'model': self.queryset.model,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -2913,23 +2914,14 @@ class InventoryItemView(generic.ObjectView):
|
|||||||
class InventoryItemEditView(generic.ObjectEditView):
|
class InventoryItemEditView(generic.ObjectEditView):
|
||||||
queryset = InventoryItem.objects.all()
|
queryset = InventoryItem.objects.all()
|
||||||
form = forms.InventoryItemForm
|
form = forms.InventoryItemForm
|
||||||
|
template_name = 'dcim/inventoryitem_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemCreateView(generic.ComponentCreateView):
|
class InventoryItemCreateView(generic.ComponentCreateView):
|
||||||
queryset = InventoryItem.objects.all()
|
queryset = InventoryItem.objects.all()
|
||||||
form = forms.InventoryItemCreateForm
|
form = forms.InventoryItemCreateForm
|
||||||
model_form = forms.InventoryItemForm
|
model_form = forms.InventoryItemForm
|
||||||
|
template_name = 'dcim/inventoryitem_edit.html'
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(InventoryItem, 'delete')
|
@register_model_view(InventoryItem, 'delete')
|
||||||
|
@ -32,7 +32,6 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_fields'),
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
label=_('Model(s)')
|
|
||||||
)
|
)
|
||||||
object_type = ContentTypeChoiceField(
|
object_type = ContentTypeChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -27,17 +27,28 @@ class Command(BaseCommand):
|
|||||||
# Return only indexers for the specified models
|
# Return only indexers for the specified models
|
||||||
else:
|
else:
|
||||||
for label in model_names:
|
for label in model_names:
|
||||||
try:
|
labels = label.lower().split('.')
|
||||||
app_label, model_name = label.lower().split('.')
|
|
||||||
except ValueError:
|
# Label specifies an exact model
|
||||||
|
if len(labels) == 2:
|
||||||
|
app_label, model_name = labels
|
||||||
|
try:
|
||||||
|
idx = registry['search'][f'{app_label}.{model_name}']
|
||||||
|
indexers[idx.model] = idx
|
||||||
|
except KeyError:
|
||||||
|
raise CommandError(f"No indexer registered for {label}")
|
||||||
|
|
||||||
|
# Label specifies all the models of an app
|
||||||
|
elif len(labels) == 1:
|
||||||
|
app_label = labels[0] + '.'
|
||||||
|
for indexer_label, idx in registry['search'].items():
|
||||||
|
if indexer_label.startswith(app_label):
|
||||||
|
indexers[idx.model] = idx
|
||||||
|
|
||||||
|
else:
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
f"Invalid model: {label}. Model names must be in the format <app_label>.<model_name>."
|
f"Invalid model: {label}. Model names must be in the format <app_label> or <app_label>.<model_name>."
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
idx = registry['search'][f'{app_label}.{model_name}']
|
|
||||||
indexers[idx.model] = idx
|
|
||||||
except KeyError:
|
|
||||||
raise CommandError(f"No indexer registered for {label}")
|
|
||||||
|
|
||||||
return indexers
|
return indexers
|
||||||
|
|
||||||
|
@ -10,7 +10,16 @@ from django.db import migrations, models
|
|||||||
def reindex(apps, schema_editor):
|
def reindex(apps, schema_editor):
|
||||||
# Build the search index (except during tests)
|
# Build the search index (except during tests)
|
||||||
if 'test' not in sys.argv:
|
if 'test' not in sys.argv:
|
||||||
management.call_command('reindex')
|
management.call_command(
|
||||||
|
'reindex',
|
||||||
|
'circuits',
|
||||||
|
'dcim',
|
||||||
|
'extras',
|
||||||
|
'ipam',
|
||||||
|
'tenancy',
|
||||||
|
'virtualization',
|
||||||
|
'wireless',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -651,7 +651,12 @@ class JobResult(models.Model):
|
|||||||
if not self.completed:
|
if not self.completed:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
duration = self.completed - self.created
|
start_time = self.started or self.created
|
||||||
|
|
||||||
|
if not start_time:
|
||||||
|
return None
|
||||||
|
|
||||||
|
duration = self.completed - start_time
|
||||||
minutes, seconds = divmod(duration.total_seconds(), 60)
|
minutes, seconds = divmod(duration.total_seconds(), 60)
|
||||||
|
|
||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
@ -33,6 +33,9 @@ class FHRPGroupTable(NetBoxTable):
|
|||||||
url_name='ipam:fhrpgroup_list'
|
url_name='ipam:fhrpgroup_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def value_ip_addresses(self, value):
|
||||||
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = FHRPGroup
|
model = FHRPGroup
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -31,6 +31,7 @@ REDIS = {
|
|||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
# 'SENTINEL_SERVICE': 'netbox',
|
||||||
|
'USERNAME': '',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
@ -44,6 +45,7 @@ REDIS = {
|
|||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
# 'SENTINEL_SERVICE': 'netbox',
|
||||||
|
'USERNAME': '',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
@ -222,6 +224,9 @@ SESSION_COOKIE_NAME = 'sessionid'
|
|||||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||||
SESSION_FILE_PATH = None
|
SESSION_FILE_PATH = None
|
||||||
|
|
||||||
|
# Localization
|
||||||
|
ENABLE_LOCALIZATION = False
|
||||||
|
|
||||||
# Time zone (default: UTC)
|
# Time zone (default: UTC)
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ REDIS = {
|
|||||||
'tasks': {
|
'tasks': {
|
||||||
'HOST': 'localhost',
|
'HOST': 'localhost',
|
||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
|
'USERNAME': '',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
@ -29,6 +30,7 @@ REDIS = {
|
|||||||
'caching': {
|
'caching': {
|
||||||
'HOST': 'localhost',
|
'HOST': 'localhost',
|
||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
|
'USERNAME': '',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
|
@ -99,8 +99,8 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
params = {
|
params = {
|
||||||
f'value__{lookup}': value
|
f'value__{lookup}': value
|
||||||
}
|
}
|
||||||
if lookup != LookupTypes.EXACT:
|
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
|
||||||
# Partial matches are valid only on string values
|
# Partial string matches are valid only on string values
|
||||||
params['type'] = FieldTypes.STRING
|
params['type'] = FieldTypes.STRING
|
||||||
if object_types:
|
if object_types:
|
||||||
params['object_type__in'] = object_types
|
params['object_type__in'] = object_types
|
||||||
|
@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.4.1'
|
VERSION = '3.4.2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -137,6 +137,7 @@ STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
|||||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
||||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||||
|
ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False)
|
||||||
|
|
||||||
# Check for hard-coded dynamic config parameters
|
# Check for hard-coded dynamic config parameters
|
||||||
for param in PARAMS:
|
for param in PARAMS:
|
||||||
@ -229,6 +230,7 @@ TASKS_REDIS_USING_SENTINEL = all([
|
|||||||
])
|
])
|
||||||
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||||
TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
|
TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
|
||||||
|
TASKS_REDIS_USERNAME = TASKS_REDIS.get('USERNAME', '')
|
||||||
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
||||||
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
||||||
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
||||||
@ -242,6 +244,8 @@ if 'caching' not in REDIS:
|
|||||||
CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
|
CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
|
||||||
CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
|
CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
|
||||||
CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
|
CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
|
||||||
|
CACHING_REDIS_USERNAME = REDIS['caching'].get('USERNAME', '')
|
||||||
|
CACHING_REDIS_USERNAME_HOST = '@'.join(filter(None, [CACHING_REDIS_USERNAME, CACHING_REDIS_HOST]))
|
||||||
CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
|
CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
|
||||||
CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
|
CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
|
||||||
CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
|
CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
|
||||||
@ -251,7 +255,7 @@ CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY',
|
|||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django_redis.cache.RedisCache',
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}',
|
'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}',
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
'PASSWORD': CACHING_REDIS_PASSWORD,
|
'PASSWORD': CACHING_REDIS_PASSWORD,
|
||||||
@ -356,6 +360,9 @@ MIDDLEWARE = [
|
|||||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if not ENABLE_LOCALIZATION:
|
||||||
|
MIDDLEWARE.remove("django.middleware.locale.LocaleMiddleware")
|
||||||
|
|
||||||
ROOT_URLCONF = 'netbox.urls'
|
ROOT_URLCONF = 'netbox.urls'
|
||||||
|
|
||||||
TEMPLATES_DIR = BASE_DIR + '/templates'
|
TEMPLATES_DIR = BASE_DIR + '/templates'
|
||||||
@ -636,6 +643,7 @@ else:
|
|||||||
}
|
}
|
||||||
RQ_PARAMS.update({
|
RQ_PARAMS.update({
|
||||||
'DB': TASKS_REDIS_DATABASE,
|
'DB': TASKS_REDIS_DATABASE,
|
||||||
|
'USERNAME': TASKS_REDIS_USERNAME,
|
||||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||||
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
|
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
|
||||||
})
|
})
|
||||||
@ -651,6 +659,13 @@ RQ_QUEUES.update({
|
|||||||
queue: RQ_PARAMS for queue in set(QUEUE_MAPPINGS.values()) if queue not in RQ_QUEUES
|
queue: RQ_PARAMS for queue in set(QUEUE_MAPPINGS.values()) if queue not in RQ_QUEUES
|
||||||
})
|
})
|
||||||
|
|
||||||
|
#
|
||||||
|
# Localization
|
||||||
|
#
|
||||||
|
|
||||||
|
if not ENABLE_LOCALIZATION:
|
||||||
|
USE_I18N = False
|
||||||
|
USE_L10N = False
|
||||||
|
|
||||||
#
|
#
|
||||||
# Plugins
|
# Plugins
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<th scope="row">Cable</th>
|
<th scope="row">Cable</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.cable|linkify }}
|
{{ object.cable|linkify }}
|
||||||
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
<a href="{% url trace_url pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -145,7 +145,7 @@
|
|||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||||
</div>
|
</div>
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:interface_trace' %}
|
||||||
{% elif object.wireless_link %}
|
{% elif object.wireless_link %}
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<tr>
|
<tr>
|
||||||
|
106
netbox/templates/dcim/inventoryitem_edit.html
Normal file
106
netbox/templates/dcim/inventoryitem_edit.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">InventoryItem</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.device %}
|
||||||
|
{% render_field form.parent %}
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.label %}
|
||||||
|
{% render_field form.role %}
|
||||||
|
{% render_field form.description %}
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Hardware</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.manufacturer %}
|
||||||
|
{% render_field form.part_id %}
|
||||||
|
{% render_field form.serial %}
|
||||||
|
{% render_field form.asset_tag %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Component Assignment</h5>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2 offset-sm-3">
|
||||||
|
<ul class="nav nav-pills" role="tablist">
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="consoleport_tab" data-bs-toggle="tab" aria-controls="consoleport" data-bs-target="#consoleport" class="nav-link {% if form.initial.consoleport or form.no_component %}active{% endif %}">
|
||||||
|
Console Port
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="consoleserverport_tab" data-bs-toggle="tab" aria-controls="consoleserverport" data-bs-target="#consoleserverport" class="nav-link {% if form.initial.consoleserverport %}active{% endif %}">
|
||||||
|
Console Server Port
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="frontport_tab" data-bs-toggle="tab" aria-controls="frontport" data-bs-target="#frontport" class="nav-link {% if form.initial.frontport %}active{% endif %}">
|
||||||
|
Front Port
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
|
||||||
|
Interface
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="poweroutlet_tab" data-bs-toggle="tab" aria-controls="poweroutlet" data-bs-target="#poweroutlet" class="nav-link {% if form.initial.poweroutlet %}active{% endif %}">
|
||||||
|
Power Outlet
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="powerport_tab" data-bs-toggle="tab" aria-controls="powerport" data-bs-target="#powerport" class="nav-link {% if form.initial.powerport %}active{% endif %}">
|
||||||
|
Power Port
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button role="tab" type="button" id="rearport_tab" data-bs-toggle="tab" aria-controls="rearport" data-bs-target="#rearport" class="nav-link {% if form.initial.rearport %}active{% endif %}">
|
||||||
|
Rear Port
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content p-0 border-0">
|
||||||
|
<div class="tab-pane {% if form.initial.consoleport or form.no_component %}active{% endif %}" id="consoleport" role="tabpanel" aria-labeled-by="consoleport_tab">
|
||||||
|
{% render_field form.consoleport %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.consoleserverport %}active{% endif %}" id="consoleserverport" role="tabpanel" aria-labeled-by="consoleserverport_tab">
|
||||||
|
{% render_field form.consoleserverport %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.frontport %}active{% endif %}" id="frontport" role="tabpanel" aria-labeled-by="frontport_tab">
|
||||||
|
{% render_field form.frontport %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
|
||||||
|
{% render_field form.interface %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.poweroutlet %}active{% endif %}" id="poweroutlet" role="tabpanel" aria-labeled-by="poweroutlet_tab">
|
||||||
|
{% render_field form.poweroutlet %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.powerport %}active{% endif %}" id="powerport" role="tabpanel" aria-labeled-by="powerport_tab">
|
||||||
|
{% render_field form.powerport %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane {% if form.initial.rearport %}active{% endif %}" id="rearport" role="tabpanel" aria-labeled-by="rearport_tab">
|
||||||
|
{% render_field form.rearport %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||||
|
</div>
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -112,7 +112,7 @@
|
|||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||||
</div>
|
</div>
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerfeed_trace' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not connected
|
Not connected
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||||
</div>
|
</div>
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:poweroutlet_trace' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||||
</div>
|
</div>
|
||||||
{% elif object.cable %}
|
{% elif object.cable %}
|
||||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerport_trace' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
|
@ -35,6 +35,10 @@
|
|||||||
{% block content-wrapper %}
|
{% block content-wrapper %}
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
|
||||||
|
{% if filter_form %}
|
||||||
|
{% applied_filters model filter_form request.GET %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Rack elevations #}
|
{# Rack elevations #}
|
||||||
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
|
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
|
||||||
{% if page %}
|
{% if page %}
|
||||||
|
@ -55,6 +55,37 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Allocated Resources</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-gauge"></i> Virtual CPUs</th>
|
||||||
|
<td>{{ vcpus_sum|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-chip"></i> Memory</th>
|
||||||
|
<td>
|
||||||
|
{% if memory_sum %}
|
||||||
|
{{ memory_sum|humanize_megabytes }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-harddisk"></i> Disk Space</th>
|
||||||
|
<td>
|
||||||
|
{% if disk_sum %}
|
||||||
|
{{ disk_sum }} GB
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch, Sum
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -169,6 +169,9 @@ class ClusterListView(generic.ObjectListView):
|
|||||||
class ClusterView(generic.ObjectView):
|
class ClusterView(generic.ObjectView):
|
||||||
queryset = Cluster.objects.all()
|
queryset = Cluster.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
return instance.virtual_machines.aggregate(vcpus_sum=Sum('vcpus'), memory_sum=Sum('memory'), disk_sum=Sum('disk'))
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
|
@register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
|
||||||
class ClusterVirtualMachinesView(generic.ObjectChildrenView):
|
class ClusterVirtualMachinesView(generic.ObjectChildrenView):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
Django==4.1.4
|
Django==4.1.5
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
django-debug-toolbar==3.8.1
|
django-debug-toolbar==3.8.1
|
||||||
django-filter==22.1
|
django-filter==22.1
|
||||||
@ -10,7 +10,7 @@ django-prometheus==2.2.0
|
|||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
django-rich==1.4.0
|
django-rich==1.4.0
|
||||||
django-rq==2.6.0
|
django-rq==2.6.0
|
||||||
django-tables2==2.4.1
|
django-tables2==2.5.0
|
||||||
django-taggit==3.1.0
|
django-taggit==3.1.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
@ -22,10 +22,10 @@ Markdown==3.3.7
|
|||||||
mkdocs-material==8.5.11
|
mkdocs-material==8.5.11
|
||||||
mkdocstrings[python-legacy]==0.19.1
|
mkdocstrings[python-legacy]==0.19.1
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.3.0
|
Pillow==9.4.0
|
||||||
psycopg2-binary==2.9.5
|
psycopg2-binary==2.9.5
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.11.1
|
sentry-sdk==1.12.1
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core[openidconnect]==4.3.0
|
social-auth-core[openidconnect]==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Reference in New Issue
Block a user