mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -06:00
commit
f1877c0c5f
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.2.7
|
placeholder: v3.2.8
|
||||||
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.2.7
|
placeholder: v3.2.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -4,7 +4,7 @@ bleach
|
|||||||
|
|
||||||
# The Python web framework on which NetBox is built
|
# The Python web framework on which NetBox is built
|
||||||
# https://github.com/django/django
|
# https://github.com/django/django
|
||||||
Django
|
Django<4.1
|
||||||
|
|
||||||
# Django middleware which permits cross-domain API requests
|
# Django middleware which permits cross-domain API requests
|
||||||
# https://github.com/OttoYiu/django-cors-headers
|
# https://github.com/OttoYiu/django-cors-headers
|
||||||
|
@ -1,5 +1,33 @@
|
|||||||
# NetBox v3.2
|
# NetBox v3.2
|
||||||
|
|
||||||
|
## v3.2.8 (2022-08-08)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name
|
||||||
|
* [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form
|
||||||
|
* [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table
|
||||||
|
* [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table
|
||||||
|
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
|
||||||
|
* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
|
||||||
|
* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
|
||||||
|
* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation
|
||||||
|
* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
|
||||||
|
* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
|
||||||
|
* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
|
||||||
|
* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
|
||||||
|
* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables
|
||||||
|
* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user
|
||||||
|
* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request
|
||||||
|
* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL
|
||||||
|
* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.2.7 (2022-07-20)
|
## v3.2.7 (2022-07-20)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -287,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('User', ('user_id',)),
|
('User', ('user_id',)),
|
||||||
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -295,25 +295,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Region')
|
label=_('Region')
|
||||||
)
|
)
|
||||||
site_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'region_id': '$region_id'
|
|
||||||
},
|
|
||||||
label=_('Site')
|
|
||||||
)
|
|
||||||
site_group_id = DynamicModelMultipleChoiceField(
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Site group')
|
label=_('Site group')
|
||||||
)
|
)
|
||||||
location_id = DynamicModelMultipleChoiceField(
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.prefetch_related('site'),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region_id',
|
||||||
|
'group_id': '$site_group_id',
|
||||||
|
},
|
||||||
|
label=_('Site')
|
||||||
|
)
|
||||||
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_id',
|
||||||
|
},
|
||||||
label=_('Location'),
|
label=_('Location'),
|
||||||
null_option='None'
|
null_option='None'
|
||||||
)
|
)
|
||||||
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_id',
|
||||||
|
'location_id': '$location_id',
|
||||||
|
},
|
||||||
|
label=_('Rack')
|
||||||
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -321,7 +321,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm):
|
|||||||
"""
|
"""
|
||||||
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
|
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
|
||||||
"""
|
"""
|
||||||
|
name_pattern = ExpandableNameField(
|
||||||
|
label='Name',
|
||||||
|
help_text="""
|
||||||
|
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
|
||||||
|
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>. {module} is accepted as a substitution for
|
||||||
|
the module bay position.
|
||||||
|
"""
|
||||||
|
)
|
||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
|
@ -146,7 +146,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
|
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
|
|||||||
related_name='%(class)ss'
|
related_name='%(class)ss'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64,
|
||||||
|
help_text="""
|
||||||
|
{module} is accepted as a substitution for the module bay position when attached to a module type.
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
_name = NaturalOrderingField(
|
_name = NaturalOrderingField(
|
||||||
target_field='name',
|
target_field='name',
|
||||||
@ -157,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -185,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplate(ModularComponentTemplateModel):
|
class PowerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -236,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'maximum_draw': self.maximum_draw,
|
||||||
|
'allocated_draw': self.allocated_draw,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplate(ModularComponentTemplateModel):
|
class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -298,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'power_port': self.power_port.name if self.power_port else None,
|
||||||
|
'feed_leg': self.feed_leg,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplate(ModularComponentTemplateModel):
|
class InterfaceTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -337,6 +376,15 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'mgmt_only': self.mgmt_only,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -410,6 +458,17 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'color': self.color,
|
||||||
|
'rear_port': self.rear_port.name,
|
||||||
|
'rear_port_position': self.rear_port_position,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplate(ModularComponentTemplateModel):
|
class RearPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -449,6 +508,16 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'color': self.color,
|
||||||
|
'positions': self.positions,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplate(ComponentTemplateModel):
|
class ModuleBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -474,6 +543,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
|||||||
position=self.position
|
position=self.position
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'label': self.label,
|
||||||
|
'position': self.position,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplate(ComponentTemplateModel):
|
class DeviceBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -498,6 +575,13 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
|
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -161,115 +159,54 @@ class DeviceType(NetBoxModel):
|
|||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = OrderedDict((
|
data = {
|
||||||
('manufacturer', self.manufacturer.name),
|
'manufacturer': self.manufacturer.name,
|
||||||
('model', self.model),
|
'model': self.model,
|
||||||
('slug', self.slug),
|
'slug': self.slug,
|
||||||
('part_number', self.part_number),
|
'part_number': self.part_number,
|
||||||
('u_height', self.u_height),
|
'u_height': self.u_height,
|
||||||
('is_full_depth', self.is_full_depth),
|
'is_full_depth': self.is_full_depth,
|
||||||
('subdevice_role', self.subdevice_role),
|
'subdevice_role': self.subdevice_role,
|
||||||
('airflow', self.airflow),
|
'airflow': self.airflow,
|
||||||
('comments', self.comments),
|
'comments': self.comments,
|
||||||
))
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
if self.consoleporttemplates.exists():
|
if self.consoleporttemplates.exists():
|
||||||
data['console-ports'] = [
|
data['console-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.consoleserverporttemplates.exists():
|
if self.consoleserverporttemplates.exists():
|
||||||
data['console-server-ports'] = [
|
data['console-server-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleserverporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.powerporttemplates.exists():
|
if self.powerporttemplates.exists():
|
||||||
data['power-ports'] = [
|
data['power-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.powerporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'maximum_draw': c.maximum_draw,
|
|
||||||
'allocated_draw': c.allocated_draw,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.powerporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.poweroutlettemplates.exists():
|
if self.poweroutlettemplates.exists():
|
||||||
data['power-outlets'] = [
|
data['power-outlets'] = [
|
||||||
{
|
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'power_port': c.power_port.name if c.power_port else None,
|
|
||||||
'feed_leg': c.feed_leg,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.poweroutlettemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.interfacetemplates.exists():
|
if self.interfacetemplates.exists():
|
||||||
data['interfaces'] = [
|
data['interfaces'] = [
|
||||||
{
|
c.to_yaml() for c in self.interfacetemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'mgmt_only': c.mgmt_only,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.interfacetemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.frontporttemplates.exists():
|
if self.frontporttemplates.exists():
|
||||||
data['front-ports'] = [
|
data['front-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.frontporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'rear_port': c.rear_port.name,
|
|
||||||
'rear_port_position': c.rear_port_position,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.frontporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.rearporttemplates.exists():
|
if self.rearporttemplates.exists():
|
||||||
data['rear-ports'] = [
|
data['rear-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.rearporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'positions': c.positions,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.rearporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.modulebaytemplates.exists():
|
if self.modulebaytemplates.exists():
|
||||||
data['module-bays'] = [
|
data['module-bays'] = [
|
||||||
{
|
c.to_yaml() for c in self.modulebaytemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'label': c.label,
|
|
||||||
'position': c.position,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.modulebaytemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.devicebaytemplates.exists():
|
if self.devicebaytemplates.exists():
|
||||||
data['device-bays'] = [
|
data['device-bays'] = [
|
||||||
{
|
c.to_yaml() for c in self.devicebaytemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.devicebaytemplates.all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return yaml.dump(dict(data), sort_keys=False)
|
return yaml.dump(dict(data), sort_keys=False)
|
||||||
@ -395,91 +332,41 @@ class ModuleType(NetBoxModel):
|
|||||||
return reverse('dcim:moduletype', args=[self.pk])
|
return reverse('dcim:moduletype', args=[self.pk])
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = OrderedDict((
|
data = {
|
||||||
('manufacturer', self.manufacturer.name),
|
'manufacturer': self.manufacturer.name,
|
||||||
('model', self.model),
|
'model': self.model,
|
||||||
('part_number', self.part_number),
|
'part_number': self.part_number,
|
||||||
('comments', self.comments),
|
'comments': self.comments,
|
||||||
))
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
if self.consoleporttemplates.exists():
|
if self.consoleporttemplates.exists():
|
||||||
data['console-ports'] = [
|
data['console-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.consoleserverporttemplates.exists():
|
if self.consoleserverporttemplates.exists():
|
||||||
data['console-server-ports'] = [
|
data['console-server-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleserverporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.powerporttemplates.exists():
|
if self.powerporttemplates.exists():
|
||||||
data['power-ports'] = [
|
data['power-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.powerporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'maximum_draw': c.maximum_draw,
|
|
||||||
'allocated_draw': c.allocated_draw,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.powerporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.poweroutlettemplates.exists():
|
if self.poweroutlettemplates.exists():
|
||||||
data['power-outlets'] = [
|
data['power-outlets'] = [
|
||||||
{
|
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'power_port': c.power_port.name if c.power_port else None,
|
|
||||||
'feed_leg': c.feed_leg,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.poweroutlettemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.interfacetemplates.exists():
|
if self.interfacetemplates.exists():
|
||||||
data['interfaces'] = [
|
data['interfaces'] = [
|
||||||
{
|
c.to_yaml() for c in self.interfacetemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'mgmt_only': c.mgmt_only,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.interfacetemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.frontporttemplates.exists():
|
if self.frontporttemplates.exists():
|
||||||
data['front-ports'] = [
|
data['front-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.frontporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'rear_port': c.rear_port.name,
|
|
||||||
'rear_port_position': c.rear_port_position,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.frontporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.rearporttemplates.exists():
|
if self.rearporttemplates.exists():
|
||||||
data['rear-ports'] = [
|
data['rear-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.rearporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'positions': c.positions,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.rearporttemplates.all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return yaml.dump(dict(data), sort_keys=False)
|
return yaml.dump(dict(data), sort_keys=False)
|
||||||
|
@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Module Type'
|
verbose_name='Module Type'
|
||||||
)
|
)
|
||||||
|
manufacturer = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
instance_count = columns.LinkedCountColumn(
|
instance_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:module_list',
|
viewname='dcim:module_list',
|
||||||
url_params={'module_type_id': 'pk'},
|
url_params={'module_type_id': 'pk'},
|
||||||
@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable):
|
|||||||
module_bay = tables.Column(
|
module_bay = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
manufacturer = tables.Column(
|
||||||
|
accessor=tables.A('module_type__manufacturer'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
module_type = tables.Column(
|
module_type = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Module
|
model = Module
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
|
||||||
|
'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag',
|
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,9 @@ class PowerPanelTable(NetBoxTable):
|
|||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
location = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
powerfeed_count = columns.LinkedCountColumn(
|
powerfeed_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:powerfeed_list',
|
viewname='dcim:powerfeed_list',
|
||||||
url_params={'power_panel_id': 'pk'},
|
url_params={'power_panel_id': 'pk'},
|
||||||
@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable):
|
|||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
accessor=Accessor('rack__site'),
|
accessor=Accessor('rack__site'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
location = tables.Column(
|
||||||
|
accessor=Accessor('rack__location'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
rack = tables.Column(
|
rack = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
|
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
|
||||||
'actions', 'created', 'last_updated',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||||
|
@ -194,14 +194,14 @@ class RackTestCase(TestCase):
|
|||||||
# Validate inventory (front face)
|
# Validate inventory (front face)
|
||||||
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
|
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
|
||||||
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
||||||
del(rack1_inventory_front[-10])
|
del rack1_inventory_front[-10]
|
||||||
for u in rack1_inventory_front:
|
for u in rack1_inventory_front:
|
||||||
self.assertIsNone(u['device'])
|
self.assertIsNone(u['device'])
|
||||||
|
|
||||||
# Validate inventory (rear face)
|
# Validate inventory (rear face)
|
||||||
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
|
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
|
||||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||||
del(rack1_inventory_rear[-10])
|
del rack1_inventory_rear[-10]
|
||||||
for u in rack1_inventory_rear:
|
for u in rack1_inventory_rear:
|
||||||
self.assertIsNone(u['device'])
|
self.assertIsNone(u['device'])
|
||||||
|
|
||||||
|
@ -2707,6 +2707,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
|
|||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
patterned_fields = ('name', 'label', 'position')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
||||||
@ -3082,7 +3083,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
|||||||
if membership_form.is_valid():
|
if membership_form.is_valid():
|
||||||
|
|
||||||
membership_form.save()
|
membership_form.save()
|
||||||
msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
|
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, mark_safe(msg))
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
@ -3127,8 +3128,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
|||||||
# Protect master device from being removed
|
# Protect master device from being removed
|
||||||
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
||||||
if virtual_chassis is not None:
|
if virtual_chassis is not None:
|
||||||
msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device))
|
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
|
||||||
messages.error(request, mark_safe(msg))
|
|
||||||
return redirect(device.get_absolute_url())
|
return redirect(device.get_absolute_url())
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -133,6 +133,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'http_method': StaticSelect(),
|
'http_method': StaticSelect(),
|
||||||
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
|
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
model = ct.model_class()
|
model = ct.model_class()
|
||||||
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
|
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
del(instance.custom_field_data[self.name])
|
del instance.custom_field_data[self.name]
|
||||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||||
|
|
||||||
def rename_object_data(self, old_name, new_name):
|
def rename_object_data(self, old_name, new_name):
|
||||||
|
@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase):
|
|||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
del(site.cf['bar'])
|
del site.cf['bar']
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
def test_missing_required_field(self):
|
def test_missing_required_field(self):
|
||||||
|
@ -30,4 +30,4 @@ class RegistryTest(TestCase):
|
|||||||
reg['foo'] = 123
|
reg['foo'] = 123
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
del(reg['foo'])
|
del reg['foo']
|
||||||
|
@ -848,7 +848,7 @@ class ServiceCreateForm(ServiceForm):
|
|||||||
# Fields which may be populated from a ServiceTemplate are not required
|
# Fields which may be populated from a ServiceTemplate are not required
|
||||||
for field in ('name', 'protocol', 'ports'):
|
for field in ('name', 'protocol', 'ports'):
|
||||||
self.fields[field].required = False
|
self.fields[field].required = False
|
||||||
del(self.fields[field].widget.attrs['required'])
|
del self.fields[field].widget.attrs['required']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data['service_template']:
|
if self.cleaned_data['service_template']:
|
||||||
|
@ -373,7 +373,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
|
|||||||
|
|
||||||
# Cache the original prefix and VRF so we can check if they have changed on post_save
|
# Cache the original prefix and VRF so we can check if they have changed on post_save
|
||||||
self._prefix = self.prefix
|
self._prefix = self.prefix
|
||||||
self._vrf = self.vrf
|
self._vrf_id = self.vrf_id
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.prefix)
|
return str(self.prefix)
|
||||||
|
@ -30,14 +30,14 @@ def update_children_depth(prefix):
|
|||||||
def handle_prefix_saved(instance, created, **kwargs):
|
def handle_prefix_saved(instance, created, **kwargs):
|
||||||
|
|
||||||
# Prefix has changed (or new instance has been created)
|
# Prefix has changed (or new instance has been created)
|
||||||
if created or instance.vrf != instance._vrf or instance.prefix != instance._prefix:
|
if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix:
|
||||||
|
|
||||||
update_parents_children(instance)
|
update_parents_children(instance)
|
||||||
update_children_depth(instance)
|
update_children_depth(instance)
|
||||||
|
|
||||||
# If this is not a new prefix, clean up parent/children of previous prefix
|
# If this is not a new prefix, clean up parent/children of previous prefix
|
||||||
if not created:
|
if not created:
|
||||||
old_prefix = Prefix(vrf=instance._vrf, prefix=instance._prefix)
|
old_prefix = Prefix(vrf_id=instance._vrf_id, prefix=instance._prefix)
|
||||||
update_parents_children(old_prefix)
|
update_parents_children(old_prefix)
|
||||||
update_children_depth(old_prefix)
|
update_children_depth(old_prefix)
|
||||||
|
|
||||||
|
@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='NAT (Inside)'
|
verbose_name='NAT (Inside)'
|
||||||
)
|
)
|
||||||
|
nat_outside = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='NAT (Outside)'
|
||||||
|
)
|
||||||
assigned = columns.BooleanColumn(
|
assigned = columns.BooleanColumn(
|
||||||
accessor='assigned_object_id',
|
accessor='assigned_object_id',
|
||||||
linkify=True,
|
linkify=True,
|
||||||
@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
|
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description',
|
||||||
'tags', 'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -333,14 +333,18 @@ class AggregateBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditView(generic.BulkEditView):
|
class AggregateBulkEditView(generic.BulkEditView):
|
||||||
queryset = Aggregate.objects.prefetch_related('rir')
|
queryset = Aggregate.objects.annotate(
|
||||||
|
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||||
|
)
|
||||||
filterset = filtersets.AggregateFilterSet
|
filterset = filtersets.AggregateFilterSet
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
form = forms.AggregateBulkEditForm
|
form = forms.AggregateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkDeleteView(generic.BulkDeleteView):
|
class AggregateBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Aggregate.objects.prefetch_related('rir')
|
queryset = Aggregate.objects.annotate(
|
||||||
|
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||||
|
)
|
||||||
filterset = filtersets.AggregateFilterSet
|
filterset = filtersets.AggregateFilterSet
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
|
|
||||||
|
@ -89,9 +89,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# An MPTT model cannot be its own parent
|
# An MPTT model cannot be its own parent
|
||||||
if self.pk and self.parent_id == self.pk:
|
if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"parent": "Cannot assign self as parent."
|
"parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.2.7'
|
VERSION = '3.2.8'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
|
|||||||
from django.db.models import DateField, DateTimeField
|
from django.db.models import DateField, DateTimeField
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2.columns import library
|
from django_tables2.columns import library
|
||||||
@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _likify_item(item):
|
def _likify_item(item):
|
||||||
if hasattr(item, 'get_absolute_url'):
|
if hasattr(item, 'get_absolute_url'):
|
||||||
return f'<a href="{item.get_absolute_url()}">{item}</a>'
|
return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
|
||||||
return item
|
return escape(item)
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
|
||||||
@ -437,13 +438,13 @@ class CustomFieldColumn(tables.Column):
|
|||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
|
||||||
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
||||||
return mark_safe(f'<a href="{value}">{value}</a>')
|
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
return ', '.join(v for v in value)
|
return ', '.join(v for v in value)
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
return mark_safe(', '.join([
|
return mark_safe(', '.join(
|
||||||
self._likify_item(obj) for obj in self.customfield.deserialize(value)
|
self._likify_item(obj) for obj in self.customfield.deserialize(value)
|
||||||
]))
|
))
|
||||||
if value is not None:
|
if value is not None:
|
||||||
obj = self.customfield.deserialize(value)
|
obj = self.customfield.deserialize(value)
|
||||||
return mark_safe(self._likify_item(obj))
|
return mark_safe(self._likify_item(obj))
|
||||||
|
@ -795,6 +795,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
model_form = None
|
model_form = None
|
||||||
filterset = None
|
filterset = None
|
||||||
table = None
|
table = None
|
||||||
|
patterned_fields = ('name', 'label')
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
||||||
@ -830,16 +831,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
|
|
||||||
for obj in data['pk']:
|
for obj in data['pk']:
|
||||||
|
|
||||||
names = data['name_pattern']
|
pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
|
||||||
labels = data['label_pattern'] if 'label_pattern' in data else None
|
for i in range(pattern_count):
|
||||||
for i, name in enumerate(names):
|
|
||||||
label = labels[i] if labels else None
|
|
||||||
|
|
||||||
component_data = {
|
component_data = {
|
||||||
self.parent_field: obj.pk,
|
self.parent_field: obj.pk
|
||||||
'name': name,
|
|
||||||
'label': label
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for field_name in self.patterned_fields:
|
||||||
|
if data.get(f'{field_name}_pattern'):
|
||||||
|
component_data[field_name] = data[f'{field_name}_pattern'][i]
|
||||||
|
|
||||||
component_data.update(data)
|
component_data.update(data)
|
||||||
component_form = self.model_form(component_data)
|
component_form = self.model_form(component_data)
|
||||||
if component_form.is_valid():
|
if component_form.is_valid():
|
||||||
|
@ -386,10 +386,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
)
|
)
|
||||||
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
||||||
if hasattr(obj, 'get_absolute_url'):
|
if hasattr(obj, 'get_absolute_url'):
|
||||||
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
|
msg = mark_safe(f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>')
|
||||||
else:
|
else:
|
||||||
msg = '{} {}'.format(msg, escape(obj))
|
msg = f'{msg} {obj}'
|
||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, msg)
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
redirect_url = request.path
|
redirect_url = request.path
|
||||||
|
@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
|
|||||||
# Workaround for schema generation (drf_yasg)
|
# Workaround for schema generation (drf_yasg)
|
||||||
if getattr(self, 'swagger_fake_view', False):
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return queryset.none()
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.is_superuser:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
|
|||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
|
|
||||||
# Authenticate the user account based on the provided credentials
|
# Authenticate the user account based on the provided credentials
|
||||||
user = authenticate(
|
username = serializer.data.get('username')
|
||||||
request=request,
|
password = serializer.data.get('password')
|
||||||
username=serializer.data['username'],
|
if not username or not password:
|
||||||
password=serializer.data['password']
|
raise AuthenticationFailed("Username and password must be provided to provision a token.")
|
||||||
)
|
user = authenticate(request=request, username=username, password=password)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise AuthenticationFailed("Invalid username/password")
|
raise AuthenticationFailed("Invalid username/password")
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
|
|||||||
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.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from social_core.backends.utils import load_backends
|
from social_core.backends.utils import load_backends
|
||||||
@ -91,7 +92,7 @@ class LoginView(View):
|
|||||||
data = request.POST if request.method == "POST" else request.GET
|
data = request.POST if request.method == "POST" else request.GET
|
||||||
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
|
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
if redirect_url and redirect_url.startswith('/'):
|
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
|
||||||
logger.debug(f"Redirecting user to {redirect_url}")
|
logger.debug(f"Redirecting user to {redirect_url}")
|
||||||
else:
|
else:
|
||||||
if redirect_url:
|
if redirect_url:
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
{% if utilization == 0 %}
|
|
||||||
<div class="progress align-items-center justify-content-center">
|
|
||||||
<span class="w-100 text-center">{{ utilization }}%</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
@ -12,10 +7,9 @@
|
|||||||
class="progress-bar {{ bar_class }}"
|
class="progress-bar {{ bar_class }}"
|
||||||
style="width: {{ utilization }}%;"
|
style="width: {{ utilization }}%;"
|
||||||
>
|
>
|
||||||
{% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %}
|
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if utilization < 25 %}
|
{% if utilization < 35 %}
|
||||||
<span class="ps-1">{{ utilization|floatformat:0 }}%</span>
|
<span class="ps-1">{{ utilization|floatformat:1 }}%</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
@ -86,8 +86,8 @@ def placeholder(value):
|
|||||||
"""
|
"""
|
||||||
if value not in ('', None):
|
if value not in ('', None):
|
||||||
return value
|
return value
|
||||||
placeholder = '<span class="text-muted">—</span>'
|
|
||||||
return mark_safe(placeholder)
|
return mark_safe('<span class="text-muted">—</span>')
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
|
@ -109,9 +109,7 @@ def annotated_date(date_value):
|
|||||||
long_ts = date(date_value, 'DATETIME_FORMAT')
|
long_ts = date(date_value, 'DATETIME_FORMAT')
|
||||||
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
|
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
|
||||||
|
|
||||||
span = f'<span title="{long_ts}">{short_ts}</span>'
|
return mark_safe(f'<span title="{long_ts}">{short_ts}</span>')
|
||||||
|
|
||||||
return mark_safe(span)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
@ -148,7 +148,7 @@ def serialize_object(obj, extra=None):
|
|||||||
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
|
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
|
||||||
if is_taggable(obj):
|
if is_taggable(obj):
|
||||||
tags = getattr(obj, '_tags', None) or obj.tags.all()
|
tags = getattr(obj, '_tags', None) or obj.tags.all()
|
||||||
data['tags'] = [tag.name for tag in tags]
|
data['tags'] = sorted([tag.name for tag in tags])
|
||||||
|
|
||||||
# Append any extra data
|
# Append any extra data
|
||||||
if extra is not None:
|
if extra is not None:
|
||||||
|
@ -48,6 +48,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
order_by=('primary_ip4', 'primary_ip6'),
|
order_by=('primary_ip4', 'primary_ip6'),
|
||||||
verbose_name='IP Address'
|
verbose_name='IP Address'
|
||||||
)
|
)
|
||||||
|
contacts = columns.ManyToManyColumn(
|
||||||
|
linkify_item=True
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:virtualmachine_list'
|
url_name='virtualization:virtualmachine_list'
|
||||||
)
|
)
|
||||||
@ -55,8 +58,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk',
|
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory',
|
||||||
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created',
|
||||||
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
||||||
@ -77,9 +81,6 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
vrf = tables.Column(
|
vrf = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:vminterface_list'
|
url_name='virtualization:vminterface_list'
|
||||||
)
|
)
|
||||||
@ -88,8 +89,7 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'contacts', 'created',
|
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||||
'last_updated',
|
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
Django==4.0.6
|
Django==4.0.7
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
django-debug-toolbar==3.5.0
|
django-debug-toolbar==3.5.0
|
||||||
django-filter==22.1
|
django-filter==22.1
|
||||||
@ -13,22 +13,22 @@ django-tables2==2.4.1
|
|||||||
django-taggit==2.1.0
|
django-taggit==2.1.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.21.3
|
||||||
graphene-django==2.15.0
|
graphene-django==2.15.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.4.1
|
||||||
markdown-include==0.6.0
|
markdown-include==0.7.0
|
||||||
mkdocs-material==8.3.9
|
mkdocs-material==8.3.9
|
||||||
mkdocstrings[python-legacy]==0.19.0
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.7.0
|
sentry-sdk==1.9.2
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core==4.3.0
|
||||||
svgwrite==1.4.2
|
svgwrite==1.4.3
|
||||||
tablib==3.2.1
|
tablib==3.2.1
|
||||||
tzdata==2022.1
|
tzdata==2022.1
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user