mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
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.3
|
placeholder: v3.2.4
|
||||||
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.3
|
placeholder: v3.2.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v4
|
- uses: actions/stale@v5
|
||||||
with:
|
with:
|
||||||
close-issue-message: >
|
close-issue-message: >
|
||||||
This issue has been automatically closed due to lack of activity. In an
|
This issue has been automatically closed due to lack of activity. In an
|
||||||
|
@ -66,6 +66,14 @@ CORS_ORIGIN_WHITELIST = [
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## CSRF_COOKIE_NAME
|
||||||
|
|
||||||
|
Default: `csrftoken`
|
||||||
|
|
||||||
|
The name of the cookie to use for the cross-site request forgery (CSRF) authentication token. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-name) for more detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CSRF_TRUSTED_ORIGINS
|
## CSRF_TRUSTED_ORIGINS
|
||||||
|
|
||||||
Default: `[]`
|
Default: `[]`
|
||||||
|
@ -85,4 +85,5 @@ The table column classes listed below are supported for use in plugins. These cl
|
|||||||
|
|
||||||
::: netbox.tables.TemplateColumn
|
::: netbox.tables.TemplateColumn
|
||||||
selection:
|
selection:
|
||||||
members: false
|
members:
|
||||||
|
- __init__
|
||||||
|
@ -1,6 +1,33 @@
|
|||||||
# NetBox v3.2
|
# NetBox v3.2
|
||||||
|
|
||||||
## v3.2.4 (FUTURE)
|
## v3.2.5 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.2.4 (2022-05-31)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8374](https://github.com/netbox-community/netbox/issues/8374) - Display device type and asset tag if name is blank but asset tag is populated
|
||||||
|
* [#8922](https://github.com/netbox-community/netbox/issues/8922) - Add service list to IP address view
|
||||||
|
* [#9098](https://github.com/netbox-community/netbox/issues/9098) - Add "other" types for power ports/outlets, pass-through ports
|
||||||
|
* [#9239](https://github.com/netbox-community/netbox/issues/9239) - Enable filtering by contact group for all models which support contact assignment
|
||||||
|
* [#9277](https://github.com/netbox-community/netbox/issues/9277) - Introduce `CSRF_COOKIE_NAME` configuration parameter
|
||||||
|
* [#9347](https://github.com/netbox-community/netbox/issues/9347) - Include services in global search
|
||||||
|
* [#9379](https://github.com/netbox-community/netbox/issues/9379) - Redirect to virtual chassis view after adding a member device
|
||||||
|
* [#9451](https://github.com/netbox-community/netbox/issues/9451) - Add `export_raw` argument for TemplateColumn
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9094](https://github.com/netbox-community/netbox/issues/9094) - Fix partial address search within Prefix and Aggregate filters
|
||||||
|
* [#9291](https://github.com/netbox-community/netbox/issues/9291) - Improve data validation for MultiObjectVar script fields
|
||||||
|
* [#9358](https://github.com/netbox-community/netbox/issues/9358) - Annotate circuit count for providers list under ASN view
|
||||||
|
* [#9387](https://github.com/netbox-community/netbox/issues/9387) - Ensure ActionsColumn `extra_buttons` are always displayed
|
||||||
|
* [#9402](https://github.com/netbox-community/netbox/issues/9402) - Fix custom field population when creating a virtual chassis
|
||||||
|
* [#9407](https://github.com/netbox-community/netbox/issues/9407) - Clean up display of prefixes values when exporting prefixes list
|
||||||
|
* [#9420](https://github.com/netbox-community/netbox/issues/9420) - Fix custom script class inheritance
|
||||||
|
* [#9425](https://github.com/netbox-community/netbox/issues/9425) - Fix bulk import for object and multi-object custom fields
|
||||||
|
* [#9430](https://github.com/netbox-community/netbox/issues/9430) - Fix passing of initial form data for DynamicModelChoiceFields
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('ASN', ('asn',)),
|
('ASN', ('asn',)),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -87,7 +87,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
('Attributes', ('type_id', 'status', 'commit_rate')),
|
('Attributes', ('type_id', 'status', 'commit_rate')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
type_id = DynamicModelMultipleChoiceField(
|
type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
|
@ -354,6 +354,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
|
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
|
||||||
# Other
|
# Other
|
||||||
TYPE_HARDWIRED = 'hardwired'
|
TYPE_HARDWIRED = 'hardwired'
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
('IEC 60320', (
|
('IEC 60320', (
|
||||||
@ -471,6 +472,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
)),
|
)),
|
||||||
('Other', (
|
('Other', (
|
||||||
(TYPE_HARDWIRED, 'Hardwired'),
|
(TYPE_HARDWIRED, 'Hardwired'),
|
||||||
|
(TYPE_OTHER, 'Other'),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -580,6 +582,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
|
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
|
||||||
# Other
|
# Other
|
||||||
TYPE_HARDWIRED = 'hardwired'
|
TYPE_HARDWIRED = 'hardwired'
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
('IEC 60320', (
|
('IEC 60320', (
|
||||||
@ -690,6 +693,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
)),
|
)),
|
||||||
('Other', (
|
('Other', (
|
||||||
(TYPE_HARDWIRED, 'Hardwired'),
|
(TYPE_HARDWIRED, 'Hardwired'),
|
||||||
|
(TYPE_OTHER, 'Other'),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1047,6 +1051,7 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
TYPE_URM_P2 = 'urm-p2'
|
TYPE_URM_P2 = 'urm-p2'
|
||||||
TYPE_URM_P4 = 'urm-p4'
|
TYPE_URM_P4 = 'urm-p4'
|
||||||
TYPE_URM_P8 = 'urm-p8'
|
TYPE_URM_P8 = 'urm-p8'
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(
|
(
|
||||||
@ -1099,6 +1104,12 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_URM_P4, 'URM-P4'),
|
(TYPE_URM_P4, 'URM-P4'),
|
||||||
(TYPE_URM_P8, 'URM-P8'),
|
(TYPE_URM_P8, 'URM-P8'),
|
||||||
(TYPE_SPLICE, 'Splice'),
|
(TYPE_SPLICE, 'Splice'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Other',
|
||||||
|
(
|
||||||
|
(TYPE_OTHER, 'Other'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -108,7 +108,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = Region
|
model = Region
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'parent_id')),
|
(None, ('q', 'tag', 'parent_id')),
|
||||||
('Contacts', ('contact', 'contact_role'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -122,7 +122,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'parent_id')),
|
(None, ('q', 'tag', 'parent_id')),
|
||||||
('Contacts', ('contact', 'contact_role'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
@ -138,7 +138,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
status = MultipleChoiceField(
|
status = MultipleChoiceField(
|
||||||
choices=SiteStatusChoices,
|
choices=SiteStatusChoices,
|
||||||
@ -168,7 +168,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
|||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
|
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -214,7 +214,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
('Function', ('status', 'role_id')),
|
('Function', ('status', 'role_id')),
|
||||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -329,7 +329,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Contacts', ('contact', 'contact_role'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@ -518,7 +518,7 @@ class DeviceFilterForm(
|
|||||||
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
||||||
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
('Components', (
|
('Components', (
|
||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||||
)),
|
)),
|
||||||
@ -788,7 +788,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -1102,7 +1102,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
|
@ -256,6 +256,8 @@ class VirtualChassisCreateForm(NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
|
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
|
||||||
raise forms.ValidationError({
|
raise forms.ValidationError({
|
||||||
'initial_position': "A position must be specified for the first VC member."
|
'initial_position': "A position must be specified for the first VC member."
|
||||||
|
@ -748,8 +748,12 @@ class Device(NetBoxModel, ConfigContextModel):
|
|||||||
return f'{self.name} ({self.asset_tag})'
|
return f'{self.name} ({self.asset_tag})'
|
||||||
elif self.name:
|
elif self.name:
|
||||||
return self.name
|
return self.name
|
||||||
|
elif self.virtual_chassis and self.asset_tag:
|
||||||
|
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.asset_tag})'
|
||||||
elif self.virtual_chassis:
|
elif self.virtual_chassis:
|
||||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||||
|
elif self.device_type and self.asset_tag:
|
||||||
|
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.asset_tag})'
|
||||||
elif self.device_type:
|
elif self.device_type:
|
||||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
@ -27,6 +27,12 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
help_text='Field data type (e.g. text, integer, etc.)'
|
help_text='Field data type (e.g. text, integer, etc.)'
|
||||||
)
|
)
|
||||||
|
object_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
|
required=False,
|
||||||
|
help_text="Object type (for object or multi-object fields)"
|
||||||
|
)
|
||||||
choices = SimpleArrayField(
|
choices = SimpleArrayField(
|
||||||
base_field=forms.CharField(),
|
base_field=forms.CharField(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -36,9 +42,9 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic',
|
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', 'weight',
|
||||||
'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
|
||||||
'ui_visibility',
|
'validation_regex', 'ui_visibility',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -306,9 +306,16 @@ class BaseScript:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _get_vars(cls):
|
def _get_vars(cls):
|
||||||
vars = {}
|
vars = {}
|
||||||
for name, attr in cls.__dict__.items():
|
|
||||||
if name not in vars and issubclass(attr.__class__, ScriptVariable):
|
# Iterate all base classes looking for ScriptVariables
|
||||||
vars[name] = attr
|
for base_class in inspect.getmro(cls):
|
||||||
|
# When object is reached there's no reason to continue
|
||||||
|
if base_class is object:
|
||||||
|
break
|
||||||
|
|
||||||
|
for name, attr in base_class.__dict__.items():
|
||||||
|
if name not in vars and issubclass(attr.__class__, ScriptVariable):
|
||||||
|
vars[name] = attr
|
||||||
|
|
||||||
# Order variables according to field_order
|
# Order variables according to field_order
|
||||||
field_order = getattr(cls.Meta, 'field_order', None)
|
field_order = getattr(cls.Meta, 'field_order', None)
|
||||||
|
@ -40,10 +40,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility',
|
'name,label,type,content_types,object_type,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility',
|
||||||
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3},read-write',
|
'field4,Field 4,text,dcim.site,,100,exact,,,,[a-z]{3},read-write',
|
||||||
'field5,Field 5,integer,dcim.site,100,exact,,1,100,,read-write',
|
'field5,Field 5,integer,dcim.site,,100,exact,,1,100,,read-write',
|
||||||
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,,read-write',
|
'field6,Field 6,select,dcim.site,,100,exact,"A,B,C",,,,read-write',
|
||||||
|
'field7,Field 7,object,dcim.site,dcim.region,100,exact,,,,,read-write',
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
@ -145,9 +145,11 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
|
qs_filter |= Q(prefix__contains=value.strip())
|
||||||
try:
|
try:
|
||||||
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
|
qs_filter |= Q(prefix__contains=value.strip())
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
@ -334,9 +336,11 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
|
qs_filter |= Q(prefix__contains=value.strip())
|
||||||
try:
|
try:
|
||||||
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
|
qs_filter |= Q(prefix__contains=value.strip())
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
@ -226,8 +226,9 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixTable(NetBoxTable):
|
class PrefixTable(NetBoxTable):
|
||||||
prefix = tables.TemplateColumn(
|
prefix = columns.TemplateColumn(
|
||||||
template_code=PREFIX_LINK,
|
template_code=PREFIX_LINK,
|
||||||
|
export_raw=True,
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
prefix_flat = tables.TemplateColumn(
|
prefix_flat = tables.TemplateColumn(
|
||||||
|
@ -4,7 +4,7 @@ from django.db.models.expressions import RawSQL
|
|||||||
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 circuits.models import Provider
|
from circuits.models import Provider, Circuit
|
||||||
from circuits.tables import ProviderTable
|
from circuits.tables import ProviderTable
|
||||||
from dcim.filtersets import InterfaceFilterSet
|
from dcim.filtersets import InterfaceFilterSet
|
||||||
from dcim.models import Interface, Site
|
from dcim.models import Interface, Site
|
||||||
@ -225,7 +225,9 @@ class ASNView(generic.ObjectView):
|
|||||||
sites_table.configure(request)
|
sites_table.configure(request)
|
||||||
|
|
||||||
# Gather assigned Providers
|
# Gather assigned Providers
|
||||||
providers = instance.providers.restrict(request.user, 'view')
|
providers = instance.providers.restrict(request.user, 'view').annotate(
|
||||||
|
count_circuits=count_related(Circuit, 'provider')
|
||||||
|
)
|
||||||
providers_table = ProviderTable(providers, user=request.user)
|
providers_table = ProviderTable(providers, user=request.user)
|
||||||
providers_table.configure(request)
|
providers_table.configure(request)
|
||||||
|
|
||||||
@ -674,11 +676,14 @@ class IPAddressView(generic.ObjectView):
|
|||||||
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
|
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
|
||||||
related_ips_table.configure(request)
|
related_ips_table.configure(request)
|
||||||
|
|
||||||
|
services = Service.objects.restrict(request.user, 'view').filter(ipaddresses=instance)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'parent_prefixes_table': parent_prefixes_table,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
'duplicate_ips_table': duplicate_ips_table,
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
'more_duplicate_ips': duplicate_ips.count() > 10,
|
'more_duplicate_ips': duplicate_ips.count() > 10,
|
||||||
'related_ips_table': related_ips_table,
|
'related_ips_table': related_ips_table,
|
||||||
|
'services': services,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,6 +202,9 @@ RQ_DEFAULT_TIMEOUT = 300
|
|||||||
# this setting is derived from the installed location.
|
# this setting is derived from the installed location.
|
||||||
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
||||||
|
|
||||||
|
# The name to use for the csrf token cookie.
|
||||||
|
CSRF_COOKIE_NAME = 'csrftoken'
|
||||||
|
|
||||||
# The name to use for the session cookie.
|
# The name to use for the session cookie.
|
||||||
SESSION_COOKIE_NAME = 'sessionid'
|
SESSION_COOKIE_NAME = 'sessionid'
|
||||||
|
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
|
import circuits.filtersets
|
||||||
|
import circuits.tables
|
||||||
|
import dcim.filtersets
|
||||||
|
import dcim.tables
|
||||||
|
import ipam.filtersets
|
||||||
|
import ipam.tables
|
||||||
|
import tenancy.filtersets
|
||||||
|
import tenancy.tables
|
||||||
|
import virtualization.filtersets
|
||||||
|
import virtualization.tables
|
||||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||||
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
|
|
||||||
from dcim.filtersets import (
|
|
||||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, LocationFilterSet, ModuleFilterSet, ModuleTypeFilterSet,
|
|
||||||
PowerFeedFilterSet, RackFilterSet, RackReservationFilterSet, SiteFilterSet, VirtualChassisFilterSet,
|
|
||||||
)
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis,
|
Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
from dcim.tables import (
|
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
||||||
CableTable, DeviceTable, DeviceTypeTable, LocationTable, ModuleTable, ModuleTypeTable, PowerFeedTable, RackTable,
|
|
||||||
RackReservationTable, SiteTable, VirtualChassisTable,
|
|
||||||
)
|
|
||||||
from ipam.filtersets import (
|
|
||||||
AggregateFilterSet, ASNFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet,
|
|
||||||
)
|
|
||||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF
|
|
||||||
from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
|
||||||
from tenancy.filtersets import ContactFilterSet, TenantFilterSet
|
|
||||||
from tenancy.models import Contact, Tenant, ContactAssignment
|
from tenancy.models import Contact, Tenant, ContactAssignment
|
||||||
from tenancy.tables import ContactTable, TenantTable
|
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
|
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from virtualization.tables import ClusterTable, VirtualMachineTable
|
|
||||||
|
|
||||||
SEARCH_MAX_RESULTS = 15
|
SEARCH_MAX_RESULTS = 15
|
||||||
|
|
||||||
@ -36,22 +28,22 @@ CIRCUIT_TYPES = OrderedDict(
|
|||||||
'queryset': Provider.objects.annotate(
|
'queryset': Provider.objects.annotate(
|
||||||
count_circuits=count_related(Circuit, 'provider')
|
count_circuits=count_related(Circuit, 'provider')
|
||||||
),
|
),
|
||||||
'filterset': ProviderFilterSet,
|
'filterset': circuits.filtersets.ProviderFilterSet,
|
||||||
'table': ProviderTable,
|
'table': circuits.tables.ProviderTable,
|
||||||
'url': 'circuits:provider_list',
|
'url': 'circuits:provider_list',
|
||||||
}),
|
}),
|
||||||
('circuit', {
|
('circuit', {
|
||||||
'queryset': Circuit.objects.prefetch_related(
|
'queryset': Circuit.objects.prefetch_related(
|
||||||
'type', 'provider', 'tenant', 'terminations__site'
|
'type', 'provider', 'tenant', 'terminations__site'
|
||||||
),
|
),
|
||||||
'filterset': CircuitFilterSet,
|
'filterset': circuits.filtersets.CircuitFilterSet,
|
||||||
'table': CircuitTable,
|
'table': circuits.tables.CircuitTable,
|
||||||
'url': 'circuits:circuit_list',
|
'url': 'circuits:circuit_list',
|
||||||
}),
|
}),
|
||||||
('providernetwork', {
|
('providernetwork', {
|
||||||
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
|
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
|
||||||
'filterset': ProviderNetworkFilterSet,
|
'filterset': circuits.filtersets.ProviderNetworkFilterSet,
|
||||||
'table': ProviderNetworkTable,
|
'table': circuits.tables.ProviderNetworkTable,
|
||||||
'url': 'circuits:providernetwork_list',
|
'url': 'circuits:providernetwork_list',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -62,22 +54,22 @@ DCIM_TYPES = OrderedDict(
|
|||||||
(
|
(
|
||||||
('site', {
|
('site', {
|
||||||
'queryset': Site.objects.prefetch_related('region', 'tenant'),
|
'queryset': Site.objects.prefetch_related('region', 'tenant'),
|
||||||
'filterset': SiteFilterSet,
|
'filterset': dcim.filtersets.SiteFilterSet,
|
||||||
'table': SiteTable,
|
'table': dcim.tables.SiteTable,
|
||||||
'url': 'dcim:site_list',
|
'url': 'dcim:site_list',
|
||||||
}),
|
}),
|
||||||
('rack', {
|
('rack', {
|
||||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate(
|
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate(
|
||||||
device_count=count_related(Device, 'rack')
|
device_count=count_related(Device, 'rack')
|
||||||
),
|
),
|
||||||
'filterset': RackFilterSet,
|
'filterset': dcim.filtersets.RackFilterSet,
|
||||||
'table': RackTable,
|
'table': dcim.tables.RackTable,
|
||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
('rackreservation', {
|
('rackreservation', {
|
||||||
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
||||||
'filterset': RackReservationFilterSet,
|
'filterset': dcim.filtersets.RackReservationFilterSet,
|
||||||
'table': RackReservationTable,
|
'table': dcim.tables.RackReservationTable,
|
||||||
'url': 'dcim:rackreservation_list',
|
'url': 'dcim:rackreservation_list',
|
||||||
}),
|
}),
|
||||||
('location', {
|
('location', {
|
||||||
@ -94,60 +86,60 @@ DCIM_TYPES = OrderedDict(
|
|||||||
'rack_count',
|
'rack_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('site'),
|
).prefetch_related('site'),
|
||||||
'filterset': LocationFilterSet,
|
'filterset': dcim.filtersets.LocationFilterSet,
|
||||||
'table': LocationTable,
|
'table': dcim.tables.LocationTable,
|
||||||
'url': 'dcim:location_list',
|
'url': 'dcim:location_list',
|
||||||
}),
|
}),
|
||||||
('devicetype', {
|
('devicetype', {
|
||||||
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||||
instance_count=count_related(Device, 'device_type')
|
instance_count=count_related(Device, 'device_type')
|
||||||
),
|
),
|
||||||
'filterset': DeviceTypeFilterSet,
|
'filterset': dcim.filtersets.DeviceTypeFilterSet,
|
||||||
'table': DeviceTypeTable,
|
'table': dcim.tables.DeviceTypeTable,
|
||||||
'url': 'dcim:devicetype_list',
|
'url': 'dcim:devicetype_list',
|
||||||
}),
|
}),
|
||||||
('device', {
|
('device', {
|
||||||
'queryset': Device.objects.prefetch_related(
|
'queryset': Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
|
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
|
||||||
),
|
),
|
||||||
'filterset': DeviceFilterSet,
|
'filterset': dcim.filtersets.DeviceFilterSet,
|
||||||
'table': DeviceTable,
|
'table': dcim.tables.DeviceTable,
|
||||||
'url': 'dcim:device_list',
|
'url': 'dcim:device_list',
|
||||||
}),
|
}),
|
||||||
('moduletype', {
|
('moduletype', {
|
||||||
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
|
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||||
instance_count=count_related(Module, 'module_type')
|
instance_count=count_related(Module, 'module_type')
|
||||||
),
|
),
|
||||||
'filterset': ModuleTypeFilterSet,
|
'filterset': dcim.filtersets.ModuleTypeFilterSet,
|
||||||
'table': ModuleTypeTable,
|
'table': dcim.tables.ModuleTypeTable,
|
||||||
'url': 'dcim:moduletype_list',
|
'url': 'dcim:moduletype_list',
|
||||||
}),
|
}),
|
||||||
('module', {
|
('module', {
|
||||||
'queryset': Module.objects.prefetch_related(
|
'queryset': Module.objects.prefetch_related(
|
||||||
'module_type__manufacturer', 'device', 'module_bay',
|
'module_type__manufacturer', 'device', 'module_bay',
|
||||||
),
|
),
|
||||||
'filterset': ModuleFilterSet,
|
'filterset': dcim.filtersets.ModuleFilterSet,
|
||||||
'table': ModuleTable,
|
'table': dcim.tables.ModuleTable,
|
||||||
'url': 'dcim:module_list',
|
'url': 'dcim:module_list',
|
||||||
}),
|
}),
|
||||||
('virtualchassis', {
|
('virtualchassis', {
|
||||||
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
|
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
|
||||||
member_count=count_related(Device, 'virtual_chassis')
|
member_count=count_related(Device, 'virtual_chassis')
|
||||||
),
|
),
|
||||||
'filterset': VirtualChassisFilterSet,
|
'filterset': dcim.filtersets.VirtualChassisFilterSet,
|
||||||
'table': VirtualChassisTable,
|
'table': dcim.tables.VirtualChassisTable,
|
||||||
'url': 'dcim:virtualchassis_list',
|
'url': 'dcim:virtualchassis_list',
|
||||||
}),
|
}),
|
||||||
('cable', {
|
('cable', {
|
||||||
'queryset': Cable.objects.all(),
|
'queryset': Cable.objects.all(),
|
||||||
'filterset': CableFilterSet,
|
'filterset': dcim.filtersets.CableFilterSet,
|
||||||
'table': CableTable,
|
'table': dcim.tables.CableTable,
|
||||||
'url': 'dcim:cable_list',
|
'url': 'dcim:cable_list',
|
||||||
}),
|
}),
|
||||||
('powerfeed', {
|
('powerfeed', {
|
||||||
'queryset': PowerFeed.objects.all(),
|
'queryset': PowerFeed.objects.all(),
|
||||||
'filterset': PowerFeedFilterSet,
|
'filterset': dcim.filtersets.PowerFeedFilterSet,
|
||||||
'table': PowerFeedTable,
|
'table': dcim.tables.PowerFeedTable,
|
||||||
'url': 'dcim:powerfeed_list',
|
'url': 'dcim:powerfeed_list',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -157,40 +149,46 @@ IPAM_TYPES = OrderedDict(
|
|||||||
(
|
(
|
||||||
('vrf', {
|
('vrf', {
|
||||||
'queryset': VRF.objects.prefetch_related('tenant'),
|
'queryset': VRF.objects.prefetch_related('tenant'),
|
||||||
'filterset': VRFFilterSet,
|
'filterset': ipam.filtersets.VRFFilterSet,
|
||||||
'table': VRFTable,
|
'table': ipam.tables.VRFTable,
|
||||||
'url': 'ipam:vrf_list',
|
'url': 'ipam:vrf_list',
|
||||||
}),
|
}),
|
||||||
('aggregate', {
|
('aggregate', {
|
||||||
'queryset': Aggregate.objects.prefetch_related('rir'),
|
'queryset': Aggregate.objects.prefetch_related('rir'),
|
||||||
'filterset': AggregateFilterSet,
|
'filterset': ipam.filtersets.AggregateFilterSet,
|
||||||
'table': AggregateTable,
|
'table': ipam.tables.AggregateTable,
|
||||||
'url': 'ipam:aggregate_list',
|
'url': 'ipam:aggregate_list',
|
||||||
}),
|
}),
|
||||||
('prefix', {
|
('prefix', {
|
||||||
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
||||||
'filterset': PrefixFilterSet,
|
'filterset': ipam.filtersets.PrefixFilterSet,
|
||||||
'table': PrefixTable,
|
'table': ipam.tables.PrefixTable,
|
||||||
'url': 'ipam:prefix_list',
|
'url': 'ipam:prefix_list',
|
||||||
}),
|
}),
|
||||||
('ipaddress', {
|
('ipaddress', {
|
||||||
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
|
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
|
||||||
'filterset': IPAddressFilterSet,
|
'filterset': ipam.filtersets.IPAddressFilterSet,
|
||||||
'table': IPAddressTable,
|
'table': ipam.tables.IPAddressTable,
|
||||||
'url': 'ipam:ipaddress_list',
|
'url': 'ipam:ipaddress_list',
|
||||||
}),
|
}),
|
||||||
('vlan', {
|
('vlan', {
|
||||||
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
|
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
|
||||||
'filterset': VLANFilterSet,
|
'filterset': ipam.filtersets.VLANFilterSet,
|
||||||
'table': VLANTable,
|
'table': ipam.tables.VLANTable,
|
||||||
'url': 'ipam:vlan_list',
|
'url': 'ipam:vlan_list',
|
||||||
}),
|
}),
|
||||||
('asn', {
|
('asn', {
|
||||||
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
|
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
|
||||||
'filterset': ASNFilterSet,
|
'filterset': ipam.filtersets.ASNFilterSet,
|
||||||
'table': ASNTable,
|
'table': ipam.tables.ASNTable,
|
||||||
'url': 'ipam:asn_list',
|
'url': 'ipam:asn_list',
|
||||||
}),
|
}),
|
||||||
|
('service', {
|
||||||
|
'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
|
||||||
|
'filterset': ipam.filtersets.ServiceFilterSet,
|
||||||
|
'table': ipam.tables.ServiceTable,
|
||||||
|
'url': 'ipam:service_list',
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -198,15 +196,15 @@ TENANCY_TYPES = OrderedDict(
|
|||||||
(
|
(
|
||||||
('tenant', {
|
('tenant', {
|
||||||
'queryset': Tenant.objects.prefetch_related('group'),
|
'queryset': Tenant.objects.prefetch_related('group'),
|
||||||
'filterset': TenantFilterSet,
|
'filterset': tenancy.filtersets.TenantFilterSet,
|
||||||
'table': TenantTable,
|
'table': tenancy.tables.TenantTable,
|
||||||
'url': 'tenancy:tenant_list',
|
'url': 'tenancy:tenant_list',
|
||||||
}),
|
}),
|
||||||
('contact', {
|
('contact', {
|
||||||
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
|
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
|
||||||
assignment_count=count_related(ContactAssignment, 'contact')),
|
assignment_count=count_related(ContactAssignment, 'contact')),
|
||||||
'filterset': ContactFilterSet,
|
'filterset': tenancy.filtersets.ContactFilterSet,
|
||||||
'table': ContactTable,
|
'table': tenancy.tables.ContactTable,
|
||||||
'url': 'tenancy:contact_list',
|
'url': 'tenancy:contact_list',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -219,16 +217,16 @@ VIRTUALIZATION_TYPES = OrderedDict(
|
|||||||
device_count=count_related(Device, 'cluster'),
|
device_count=count_related(Device, 'cluster'),
|
||||||
vm_count=count_related(VirtualMachine, 'cluster')
|
vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
),
|
),
|
||||||
'filterset': ClusterFilterSet,
|
'filterset': virtualization.filtersets.ClusterFilterSet,
|
||||||
'table': ClusterTable,
|
'table': virtualization.tables.ClusterTable,
|
||||||
'url': 'virtualization:cluster_list',
|
'url': 'virtualization:cluster_list',
|
||||||
}),
|
}),
|
||||||
('virtualmachine', {
|
('virtualmachine', {
|
||||||
'queryset': VirtualMachine.objects.prefetch_related(
|
'queryset': VirtualMachine.objects.prefetch_related(
|
||||||
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||||
),
|
),
|
||||||
'filterset': VirtualMachineFilterSet,
|
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
||||||
'table': VirtualMachineTable,
|
'table': virtualization.tables.VirtualMachineTable,
|
||||||
'url': 'virtualization:virtualmachine_list',
|
'url': 'virtualization:virtualmachine_list',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -84,6 +84,7 @@ if BASE_PATH:
|
|||||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||||
|
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
|
||||||
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
||||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||||
|
@ -90,6 +90,15 @@ class TemplateColumn(tables.TemplateColumn):
|
|||||||
"""
|
"""
|
||||||
PLACEHOLDER = mark_safe('—')
|
PLACEHOLDER = mark_safe('—')
|
||||||
|
|
||||||
|
def __init__(self, export_raw=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
export_raw: If true, data export returns the raw field value rather than the rendered template. (Default:
|
||||||
|
False)
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.export_raw = export_raw
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args, **kwargs):
|
||||||
ret = super().render(*args, **kwargs)
|
ret = super().render(*args, **kwargs)
|
||||||
if not ret.strip():
|
if not ret.strip():
|
||||||
@ -97,6 +106,10 @@ class TemplateColumn(tables.TemplateColumn):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def value(self, **kwargs):
|
def value(self, **kwargs):
|
||||||
|
if self.export_raw:
|
||||||
|
# Skip template rendering and export raw value
|
||||||
|
return kwargs.get('value')
|
||||||
|
|
||||||
ret = super().value(**kwargs)
|
ret = super().value(**kwargs)
|
||||||
if ret == self.PLACEHOLDER:
|
if ret == self.PLACEHOLDER:
|
||||||
return ''
|
return ''
|
||||||
@ -192,32 +205,35 @@ class ActionsColumn(tables.Column):
|
|||||||
model = table.Meta.model
|
model = table.Meta.model
|
||||||
request = getattr(table, 'context', {}).get('request')
|
request = getattr(table, 'context', {}).get('request')
|
||||||
url_appendix = f'?return_url={request.path}' if request else ''
|
url_appendix = f'?return_url={request.path}' if request else ''
|
||||||
|
html = ''
|
||||||
|
|
||||||
|
# Compile actions menu
|
||||||
links = []
|
links = []
|
||||||
user = getattr(request, 'user', AnonymousUser())
|
user = getattr(request, 'user', AnonymousUser())
|
||||||
for action, attrs in self.actions.items():
|
for action, attrs in self.actions.items():
|
||||||
permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}'
|
permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}'
|
||||||
if attrs.permission is None or user.has_perm(permission):
|
if attrs.permission is None or user.has_perm(permission):
|
||||||
url = reverse(get_viewname(model, action), kwargs={'pk': record.pk})
|
url = reverse(get_viewname(model, action), kwargs={'pk': record.pk})
|
||||||
links.append(f'<li><a class="dropdown-item" href="{url}{url_appendix}">'
|
links.append(
|
||||||
f'<i class="mdi mdi-{attrs.icon}"></i> {attrs.title}</a></li>')
|
f'<li><a class="dropdown-item" href="{url}{url_appendix}">'
|
||||||
|
f'<i class="mdi mdi-{attrs.icon}"></i> {attrs.title}</a></li>'
|
||||||
if not links:
|
)
|
||||||
return ''
|
if links:
|
||||||
|
html += (
|
||||||
menu = f'<span class="dropdown">' \
|
f'<span class="dropdown">'
|
||||||
f'<a class="btn btn-sm btn-secondary dropdown-toggle" href="#" type="button" data-bs-toggle="dropdown">' \
|
f'<a class="btn btn-sm btn-secondary dropdown-toggle" href="#" type="button" data-bs-toggle="dropdown">'
|
||||||
f'<i class="mdi mdi-wrench"></i></a>' \
|
f'<i class="mdi mdi-wrench"></i></a>'
|
||||||
f'<ul class="dropdown-menu">{"".join(links)}</ul></span>'
|
f'<ul class="dropdown-menu">{"".join(links)}</ul></span>'
|
||||||
|
)
|
||||||
|
|
||||||
# Render any extra buttons from template code
|
# Render any extra buttons from template code
|
||||||
if self.extra_buttons:
|
if self.extra_buttons:
|
||||||
template = Template(self.extra_buttons)
|
template = Template(self.extra_buttons)
|
||||||
context = getattr(table, "context", Context())
|
context = getattr(table, "context", Context())
|
||||||
context.update({'record': record})
|
context.update({'record': record})
|
||||||
menu = template.render(context) + menu
|
html = template.render(context) + html
|
||||||
|
|
||||||
return mark_safe(menu)
|
return mark_safe(html)
|
||||||
|
|
||||||
|
|
||||||
class ChoiceFieldColumn(tables.Column):
|
class ChoiceFieldColumn(tables.Column):
|
||||||
|
@ -15,74 +15,70 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Virtual Chassis</h5>
|
||||||
Virtual Chassis
|
<div class="card-body">
|
||||||
</h5>
|
<table class="table table-hover attr-table">
|
||||||
<div class="card-body">
|
<tr>
|
||||||
<table class="table table-hover attr-table">
|
<th scope="row">Domain</th>
|
||||||
<tr>
|
<td>{{ object.domain|placeholder }}</td>
|
||||||
<th scope="row">Domain</th>
|
</tr>
|
||||||
<td>{{ object.domain|placeholder }}</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Master</th>
|
||||||
<tr>
|
<td>{{ object.master|linkify }}</td>
|
||||||
<th scope="row">Master</th>
|
</tr>
|
||||||
<td>{{ object.master|linkify }}</td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
</div>
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% plugin_left_page object %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
|
||||||
{% plugin_left_page object %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-8">
|
<div class="col col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Members</h5>
|
||||||
Members
|
<div class="card-body">
|
||||||
</h5>
|
<table class="table table-hover attr-table">
|
||||||
<div class="card-body">
|
<tr>
|
||||||
<table class="table table-hover attr-table">
|
<th>Device</th>
|
||||||
<tr>
|
<th>Position</th>
|
||||||
<th>Device</th>
|
<th>Master</th>
|
||||||
<th>Position</th>
|
<th>Priority</th>
|
||||||
<th>Master</th>
|
</tr>
|
||||||
<th>Priority</th>
|
{% for vc_member in members %}
|
||||||
</tr>
|
<tr{% if vc_member == device %} class="info"{% endif %}>
|
||||||
{% for vc_member in members %}
|
<td>
|
||||||
<tr{% if vc_member == device %} class="info"{% endif %}>
|
{{ vc_member|linkify }}
|
||||||
<td>
|
</td>
|
||||||
{{ vc_member|linkify }}
|
<td>
|
||||||
</td>
|
{% badge vc_member.vc_position show_empty=True %}
|
||||||
<td>
|
</td>
|
||||||
{% badge vc_member.vc_position show_empty=True %}
|
<td>
|
||||||
</td>
|
{% if object.master == vc_member %}
|
||||||
<td>
|
{% checkmark True %}
|
||||||
{% if object.master == vc_member %}
|
{% endif %}
|
||||||
{% checkmark True %}
|
</td>
|
||||||
{% endif %}
|
<td>
|
||||||
</td>
|
{{ vc_member.vc_priority|placeholder }}
|
||||||
<td>
|
</td>
|
||||||
{{ vc_member.vc_priority|placeholder }}
|
</tr>
|
||||||
</td>
|
{% endfor %}
|
||||||
</tr>
|
</table>
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% if perms.dcim.change_virtualchassis %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'dcim:virtualchassis_add_member' pk=object.pk %}?site={{ object.master.site.pk }}&rack={{ object.master.rack.pk }}" class="btn btn-primary btn-sm">
|
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Member
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% plugin_right_page object %}
|
{% if perms.dcim.change_virtualchassis %}
|
||||||
|
<div class="card-footer text-end noprint">
|
||||||
|
<a href="{% url 'dcim:virtualchassis_add_member' pk=object.pk %}?site={{ object.master.site.pk }}&rack={{ object.master.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Member
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -134,6 +134,24 @@
|
|||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
|
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Services
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if services %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
{% for service in services %}
|
||||||
|
{% include 'ipam/inc/service.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">
|
||||||
|
None
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,6 +112,12 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
|||||||
queryset=ContactRole.objects.all(),
|
queryset=ContactRole.objects.all(),
|
||||||
label='Contact Role'
|
label='Contact Role'
|
||||||
)
|
)
|
||||||
|
contact_group = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=ContactGroup.objects.all(),
|
||||||
|
field_name='contacts__contact__group',
|
||||||
|
lookup_expr='in',
|
||||||
|
label='Contact group',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -32,7 +32,7 @@ class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = Tenant
|
model = Tenant
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'group_id')),
|
(None, ('q', 'tag', 'group_id')),
|
||||||
('Contacts', ('contact', 'contact_role'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
group_id = DynamicModelMultipleChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
|
@ -58,3 +58,8 @@ class ContactModelFilterForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Contact Role')
|
label=_('Contact Role')
|
||||||
)
|
)
|
||||||
|
contact_group = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ContactGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Contact Group')
|
||||||
|
)
|
||||||
|
@ -88,7 +88,12 @@ class DynamicModelChoiceMixin:
|
|||||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||||
# will be populated on-demand via the APISelect widget.
|
# will be populated on-demand via the APISelect widget.
|
||||||
data = bound_field.value()
|
data = bound_field.value()
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
|
# When the field is multiple choice pass the data as a list if it's not already
|
||||||
|
if isinstance(bound_field.field, DynamicModelMultipleChoiceField) and not type(data) is list:
|
||||||
|
data = [data]
|
||||||
|
|
||||||
field_name = getattr(self, 'to_field_name') or 'pk'
|
field_name = getattr(self, 'to_field_name') or 'pk'
|
||||||
filter = self.filter(field_name=field_name)
|
filter = self.filter(field_name=field_name)
|
||||||
try:
|
try:
|
||||||
@ -130,11 +135,12 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
|
|||||||
widget = widgets.APISelectMultiple
|
widget = widgets.APISelectMultiple
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
"""
|
value = value or []
|
||||||
When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the
|
|
||||||
string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType.
|
# When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the
|
||||||
"""
|
# string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType.
|
||||||
if self.null_option is not None and settings.FILTERS_NULL_CHOICE_VALUE in value:
|
if self.null_option is not None and settings.FILTERS_NULL_CHOICE_VALUE in value:
|
||||||
value = [v for v in value if v != settings.FILTERS_NULL_CHOICE_VALUE]
|
value = [v for v in value if v != settings.FILTERS_NULL_CHOICE_VALUE]
|
||||||
return [None, *value]
|
return [None, *value]
|
||||||
|
|
||||||
return super().clean(value)
|
return super().clean(value)
|
||||||
|
@ -29,6 +29,10 @@ class ClusterTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'tag')),
|
||||||
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
@ -38,7 +42,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
('Attributes', ('group_id', 'type_id', 'status')),
|
('Attributes', ('group_id', 'type_id', 'status')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
type_id = DynamicModelMultipleChoiceField(
|
type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
@ -91,7 +95,7 @@ class VirtualMachineFilterForm(
|
|||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
@ -18,10 +18,10 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==8.2.14
|
mkdocs-material==8.2.16
|
||||||
mkdocstrings[python-legacy]==0.18.1
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.1.0
|
Pillow==9.1.1
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.5.12
|
sentry-sdk==1.5.12
|
||||||
|
Reference in New Issue
Block a user