Fixes #20961
This commit is contained in:
parent
c384cec453
commit
e2665ef211
|
|
@ -1,6 +1,6 @@
|
|||
# Racks
|
||||
|
||||
The rack model represents a physical two- or four-post equipment rack in which [devices](./device.md) can be installed. Each rack must be assigned to a [site](./site.md), and may optionally be assigned to a [location](./location.md) within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique.
|
||||
The rack model represents a physical two- or four-post equipment rack in which [devices](./device.md) can be installed. Each rack must be assigned to a [site](./site.md), and may optionally be assigned to a [location](./location.md) within that site. Racks can also be organized by user-defined functional roles or by [rack groups](./rackgroup.md). The name and facility ID of each rack within a location must be unique.
|
||||
|
||||
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
||||
|
||||
|
|
@ -16,6 +16,10 @@ The [site](./site.md) to which the rack is assigned.
|
|||
|
||||
The [location](./location.md) within a site where the rack has been installed (optional).
|
||||
|
||||
### Rack Group
|
||||
|
||||
The [group](./rackgroup.md) used to organize racks by physical placement (optional).
|
||||
|
||||
### Name
|
||||
|
||||
The rack's name or identifier. Must be unique to the rack's location, if assigned.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# Rack Groups
|
||||
|
||||
Racks can optionally be assigned to rack groups to reflect their physical placement. Rack groups provide a secondary means of categorization alongside [locations](./location.md), which is particularly useful for datacenter operators who need to group racks by row, aisle, or similar physical arrangement while keeping them assigned to the same location, such as a cage or room. Rack groups are flat and do not form a hierarchy.
|
||||
|
||||
Rack groups can also be used to scope [VLAN groups](../ipam/vlangroup.md), which can help model L2 domains spanning rows or pairs of racks.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
|
@ -221,6 +221,7 @@ nav:
|
|||
- PowerPort: 'models/dcim/powerport.md'
|
||||
- PowerPortTemplate: 'models/dcim/powerporttemplate.md'
|
||||
- Rack: 'models/dcim/rack.md'
|
||||
- RackGroup: 'models/dcim/rackgroup.md'
|
||||
- RackReservation: 'models/dcim/rackreservation.md'
|
||||
- RackRole: 'models/dcim/rackrole.md'
|
||||
- RackType: 'models/dcim/racktype.md'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
|||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import Rack, RackReservation, RackRole, RackType
|
||||
from dcim.models import Rack, RackGroup, RackReservation, RackRole, RackType
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
|
||||
from netbox.choices import *
|
||||
|
|
@ -16,6 +16,7 @@ from .sites import LocationSerializer, SiteSerializer
|
|||
|
||||
__all__ = (
|
||||
'RackElevationDetailFilterSerializer',
|
||||
'RackGroupSerializer',
|
||||
'RackReservationSerializer',
|
||||
'RackRoleSerializer',
|
||||
'RackSerializer',
|
||||
|
|
@ -23,6 +24,20 @@ __all__ = (
|
|||
)
|
||||
|
||||
|
||||
class RackGroupSerializer(OrganizationalModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'rack_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||
|
||||
|
||||
class RackRoleSerializer(OrganizationalModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
|
|
@ -87,6 +102,11 @@ class RackSerializer(RackBaseSerializer):
|
|||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
group = RackGroupSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
tenant = TenantSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
|
|
@ -127,11 +147,11 @@ class RackSerializer(RackBaseSerializer):
|
|||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count', 'powerfeed_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'group', 'tenant',
|
||||
'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit',
|
||||
'weight', 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'airflow', 'description', 'owner', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ router.register('sites', views.SiteViewSet)
|
|||
|
||||
# Racks
|
||||
router.register('locations', views.LocationViewSet)
|
||||
router.register('rack-groups', views.RackGroupViewSet)
|
||||
router.register('rack-types', views.RackTypeViewSet)
|
||||
router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
|
|
|
|||
|
|
@ -154,6 +154,17 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
|
||||
class RackGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filterset_class = filtersets.RackGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ __all__ = (
|
|||
'PowerPortFilterSet',
|
||||
'PowerPortTemplateFilterSet',
|
||||
'RackFilterSet',
|
||||
'RackGroupFilterSet',
|
||||
'RackReservationFilterSet',
|
||||
'RackRoleFilterSet',
|
||||
'RackTypeFilterSet',
|
||||
|
|
@ -315,6 +316,14 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupMode
|
|||
return queryset
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackGroupFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
|
@ -419,6 +428,18 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
|||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Group (slug)'),
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
|
|
@ -553,6 +574,19 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
distinct=False,
|
||||
label=_('Group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack__group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Group (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackReservationStatusChoices,
|
||||
distinct=False,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ __all__ = (
|
|||
'PowerPortBulkEditForm',
|
||||
'PowerPortTemplateBulkEditForm',
|
||||
'RackBulkEditForm',
|
||||
'RackGroupBulkEditForm',
|
||||
'RackReservationBulkEditForm',
|
||||
'RackRoleBulkEditForm',
|
||||
'RackTypeBulkEditForm',
|
||||
|
|
@ -201,6 +202,14 @@ class LocationBulkEditForm(NestedGroupModelBulkEditForm):
|
|||
nullable_fields = ('parent', 'tenant', 'facility', 'description', 'comments')
|
||||
|
||||
|
||||
class RackGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
model = RackGroup
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
|
|
@ -336,6 +345,11 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
|
|||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
label=_('Group'),
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
|
|
@ -435,14 +449,16 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
|
|||
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
|
||||
FieldSet(
|
||||
'status', 'group', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')
|
||||
),
|
||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||
FieldSet('outer_width', 'outer_height', 'outer_depth', 'outer_unit', name=_('Outer Dimensions')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'mounting_depth', name=_('Hardware')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'location', 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ __all__ = (
|
|||
'PowerOutletImportForm',
|
||||
'PowerPanelImportForm',
|
||||
'PowerPortImportForm',
|
||||
'RackGroupImportForm',
|
||||
'RackImportForm',
|
||||
'RackReservationImportForm',
|
||||
'RackRoleImportForm',
|
||||
|
|
@ -187,6 +188,13 @@ class LocationImportForm(NestedGroupModelImportForm):
|
|||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class RackRoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
|
|
@ -261,6 +269,13 @@ class RackImportForm(PrimaryModelImportForm):
|
|||
to_field_name='name',
|
||||
help_text=_('Name of assigned tenant')
|
||||
)
|
||||
group = CSVModelChoiceField(
|
||||
label=_('Rack group'),
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Name of assigned group')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=RackStatusChoices,
|
||||
|
|
@ -318,10 +333,10 @@ class RackImportForm(PrimaryModelImportForm):
|
|||
class Meta:
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
|
||||
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'owner', 'comments',
|
||||
'tags',
|
||||
'site', 'location', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor',
|
||||
'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'owner',
|
||||
'comments', 'tags',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ __all__ = (
|
|||
'PowerPortTemplateFilterForm',
|
||||
'RackElevationFilterForm',
|
||||
'RackFilterForm',
|
||||
'RackGroupFilterForm',
|
||||
'RackReservationFilterForm',
|
||||
'RackRoleFilterForm',
|
||||
'RackTypeFilterForm',
|
||||
|
|
@ -276,6 +277,15 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM
|
|||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackGroupFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RackGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackRoleFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = RackRole
|
||||
fieldsets = (
|
||||
|
|
@ -355,7 +365,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
|||
model = Rack
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'group_id', name=_('Location')),
|
||||
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
|
|
@ -392,6 +402,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
|||
},
|
||||
label=_('Location')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Rack group')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=RackStatusChoices,
|
||||
|
|
@ -435,7 +451,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
|||
class RackElevationFilterForm(RackFilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'group_id', 'id', name=_('Location')),
|
||||
FieldSet('status', 'role_id', name=_('Function')),
|
||||
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
|
|
@ -459,7 +475,7 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('status', 'user_id', name=_('Reservation')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'group_id', 'rack_id', name=_('Rack')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
|
|
@ -491,10 +507,17 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
|||
label=_('Location'),
|
||||
null_option='None'
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Rack group')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$group_id',
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ __all__ = (
|
|||
'PowerPortForm',
|
||||
'PowerPortTemplateForm',
|
||||
'RackForm',
|
||||
'RackGroupForm',
|
||||
'RackReservationForm',
|
||||
'RackRoleForm',
|
||||
'RackTypeForm',
|
||||
|
|
@ -206,6 +207,18 @@ class LocationForm(TenancyForm, NestedGroupModelForm):
|
|||
)
|
||||
|
||||
|
||||
class RackGroupForm(OrganizationalModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('Rack Group')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class RackRoleForm(OrganizationalModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
|
||||
|
|
@ -263,6 +276,11 @@ class RackForm(TenancyForm, PrimaryModelForm):
|
|||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
label=_('Rack Group'),
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
label=_('Role'),
|
||||
queryset=RackRole.objects.all(),
|
||||
|
|
@ -278,7 +296,7 @@ class RackForm(TenancyForm, PrimaryModelForm):
|
|||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
|
||||
'site', 'location', 'group', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
|
||||
name=_('Rack')
|
||||
),
|
||||
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
|
||||
|
|
@ -288,7 +306,7 @@ class RackForm(TenancyForm, PrimaryModelForm):
|
|||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||
'site', 'location', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'owner', 'comments', 'tags',
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ __all__ = (
|
|||
'PowerPortFilter',
|
||||
'PowerPortTemplateFilter',
|
||||
'RackFilter',
|
||||
'RackGroupFilter',
|
||||
'RackReservationFilter',
|
||||
'RackRoleFilter',
|
||||
'RackTypeFilter',
|
||||
|
|
@ -959,6 +960,10 @@ class RackFilter(
|
|||
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group: Annotated['RackGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group_id: ID | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['RackStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
|
@ -974,6 +979,11 @@ class RackFilter(
|
|||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackGroup, lookups=True)
|
||||
class RackGroupFilter(OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.RackReservation, lookups=True)
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ class DCIMQuery:
|
|||
power_port_template: PowerPortTemplateType = strawberry_django.field()
|
||||
power_port_template_list: list[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
rack_group: RackGroupType = strawberry_django.field()
|
||||
rack_group_list: list[RackGroupType] = strawberry_django.field()
|
||||
|
||||
rack_type: RackTypeType = strawberry_django.field()
|
||||
rack_type_list: list[RackTypeType] = strawberry_django.field()
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ __all__ = (
|
|||
'PowerPanelType',
|
||||
'PowerPortTemplateType',
|
||||
'PowerPortType',
|
||||
'RackGroupType',
|
||||
'RackReservationType',
|
||||
'RackRoleType',
|
||||
'RackType',
|
||||
|
|
@ -736,6 +737,17 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
|||
poweroutlet_templates: list[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RackGroup,
|
||||
fields='__all__',
|
||||
filters=RackGroupFilter,
|
||||
pagination=True
|
||||
)
|
||||
class RackGroupType(OrganizationalObjectType):
|
||||
|
||||
racks: list[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RackType,
|
||||
fields='__all__',
|
||||
|
|
@ -756,6 +768,7 @@ class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType):
|
|||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType):
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
||||
location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
group: Annotated["RackGroupType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
role: Annotated["RackRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import netbox.models.deletion
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0226_modulebay_rebuild_tree'),
|
||||
('extras', '0134_owner'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RackGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
(
|
||||
'owner',
|
||||
models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'rack group',
|
||||
'verbose_name_plural': 'rack groups',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='group',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='racks',
|
||||
to='dcim.rackgroup',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -29,14 +29,30 @@ from .power import PowerFeed
|
|||
|
||||
__all__ = (
|
||||
'Rack',
|
||||
'RackGroup',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RackType',
|
||||
)
|
||||
|
||||
#
|
||||
# Rack Organization
|
||||
#
|
||||
|
||||
|
||||
class RackGroup(OrganizationalModel):
|
||||
"""
|
||||
Racks can be grouped by physical placement within a Location.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('rack group')
|
||||
verbose_name_plural = _('rack groups')
|
||||
|
||||
|
||||
#
|
||||
# Rack Types
|
||||
# Rack Base
|
||||
#
|
||||
|
||||
class RackBase(WeightMixin, PrimaryModel):
|
||||
|
|
@ -123,6 +139,10 @@ class RackBase(WeightMixin, PrimaryModel):
|
|||
abstract = True
|
||||
|
||||
|
||||
#
|
||||
# Rack Types
|
||||
#
|
||||
|
||||
class RackType(ImageAttachmentsMixin, RackBase):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
|
|
@ -290,6 +310,14 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
|||
blank=True,
|
||||
null=True
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='dcim.RackGroup',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('physical grouping')
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
|
|
|
|||
|
|
@ -315,6 +315,18 @@ class RackReservationIndex(SearchIndex):
|
|||
display_attrs = ('rack', 'tenant', 'user', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class RackGroupIndex(SearchIndex):
|
||||
model = models.RackGroup
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class RackRoleIndex(SearchIndex):
|
||||
model = models.RackRole
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import django_tables2 as tables
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackReservation, RackRole, RackType
|
||||
from dcim.models import Rack, RackGroup, RackReservation, RackRole, RackType
|
||||
from netbox.tables import OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from .template_code import OUTER_UNIT, WEIGHT
|
||||
|
||||
__all__ = (
|
||||
'RackGroupTable',
|
||||
'RackReservationTable',
|
||||
'RackRoleTable',
|
||||
'RackTable',
|
||||
|
|
@ -16,6 +17,29 @@ __all__ = (
|
|||
)
|
||||
|
||||
|
||||
class RackGroupTable(OrganizationalModelTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True,
|
||||
)
|
||||
rack_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:rack_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name=_('Racks'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rackgroup_list',
|
||||
)
|
||||
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = RackGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'rack_count', 'description', 'slug', 'comments', 'tags', 'actions', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'rack_count', 'description')
|
||||
|
||||
|
||||
class RackRoleTable(OrganizationalModelTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
|
|
@ -111,6 +135,10 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
|||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
verbose_name=_('Group'),
|
||||
linkify=True,
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
)
|
||||
|
|
@ -172,15 +200,15 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
|||
class Meta(PrimaryModelTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
|
||||
'pk', 'id', 'name', 'site', 'location', 'group', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
|
||||
'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
||||
'outer_height', 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments',
|
||||
'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts',
|
||||
'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'rack_type', 'u_height',
|
||||
'device_count', 'get_utilization',
|
||||
'pk', 'name', 'site', 'location', 'group', 'status', 'facility_id', 'tenant', 'role', 'rack_type',
|
||||
'u_height', 'device_count', 'get_utilization',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -200,6 +228,11 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
|
|||
accessor=Accessor('rack__location'),
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
verbose_name=_('Group'),
|
||||
accessor=Accessor('rack__group'),
|
||||
linkify=True
|
||||
)
|
||||
rack = tables.Column(
|
||||
verbose_name=_('Rack'),
|
||||
linkify=True
|
||||
|
|
@ -218,7 +251,7 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
|
|||
class Meta(PrimaryModelTable.Meta):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'group', 'rack', 'unit_list', 'status', 'user', 'created',
|
||||
'tenant', 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'status', 'user', 'description')
|
||||
|
|
|
|||
|
|
@ -280,6 +280,38 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
|||
]
|
||||
|
||||
|
||||
class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackGroup
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Rack Group 4',
|
||||
'slug': 'rack-group-4',
|
||||
},
|
||||
{
|
||||
'name': 'Rack Group 5',
|
||||
'slug': 'rack-group-5',
|
||||
},
|
||||
{
|
||||
'name': 'Rack Group 6',
|
||||
'slug': 'rack-group-6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
|
||||
class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackRole
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
|
|
@ -397,6 +429,12 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
|||
Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
|
||||
)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
rack_roles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
|
||||
RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
|
||||
|
|
@ -404,9 +442,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
|||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
racks = (
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 3'),
|
||||
Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 3'),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
|
|
@ -415,18 +453,21 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
|||
'name': 'Test Rack 4',
|
||||
'site': sites[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 5',
|
||||
'site': sites[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 6',
|
||||
'site': sites[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -534,6 +534,37 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackGroup.objects.all()
|
||||
filterset = RackGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', description='foobar1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', description='foobar2'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rack Group 1', 'Rack Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['rack-group-1', 'rack-group-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackRole.objects.all()
|
||||
filterset = RackRoleFilterSet
|
||||
|
|
@ -738,18 +769,18 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
|
|
@ -810,6 +841,13 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
)
|
||||
RackType.objects.bulk_create(rack_types)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
rack_roles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
||||
|
|
@ -838,6 +876,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
facility_id='rack-1',
|
||||
site=sites[0],
|
||||
location=locations[0],
|
||||
group=rack_groups[0],
|
||||
tenant=tenants[0],
|
||||
status=RackStatusChoices.STATUS_ACTIVE,
|
||||
role=rack_roles[0],
|
||||
|
|
@ -862,6 +901,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
facility_id='rack-2',
|
||||
site=sites[1],
|
||||
location=locations[1],
|
||||
group=rack_groups[1],
|
||||
tenant=tenants[1],
|
||||
status=RackStatusChoices.STATUS_PLANNED,
|
||||
role=rack_roles[1],
|
||||
|
|
@ -886,6 +926,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
facility_id='rack-3',
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
group=rack_groups[2],
|
||||
tenant=tenants[2],
|
||||
status=RackStatusChoices.STATUS_RESERVED,
|
||||
role=rack_roles[2],
|
||||
|
|
@ -1017,6 +1058,13 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack_group(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [rack_groups[0].slug, rack_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
|
@ -1095,18 +1143,18 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
|
|
@ -1118,10 +1166,17 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0], group=rack_groups[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2], group=rack_groups[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
|
|
@ -1207,6 +1262,13 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack_group(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [rack_groups[0].slug, rack_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [RackReservationStatusChoices.STATUS_ACTIVE, RackReservationStatusChoices.STATUS_PENDING]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
|
|
|||
|
|
@ -267,6 +267,47 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||
}
|
||||
|
||||
|
||||
class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Rack Group X',
|
||||
'slug': 'rack-group-x',
|
||||
'description': 'New group',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,description",
|
||||
"Rack Group 4,rack-group-4,Fourth group",
|
||||
"Rack Group 5,rack-group-5,Fifth group",
|
||||
"Rack Group 6,rack-group-6,",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{rack_groups[0].pk},Rack Group 7,New description7",
|
||||
f"{rack_groups[1].pk},Rack Group 8,New description8",
|
||||
f"{rack_groups[2].pk},Rack Group 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackRole
|
||||
|
||||
|
|
@ -472,6 +513,12 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2'),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
|
||||
rackroles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
||||
|
|
@ -479,8 +526,8 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
RackRole.objects.bulk_create(rackroles)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[0]),
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0], role=rackroles[0]),
|
||||
Rack(name='Rack 2', site=sites[0], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[0]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
|
@ -492,6 +539,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
'facility_id': 'Facility X',
|
||||
'site': sites[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_PLANNED,
|
||||
'role': rackroles[1].pk,
|
||||
|
|
@ -513,10 +561,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,location,name,status,width,u_height,weight,max_weight,weight_unit",
|
||||
"Site 1,,Rack 4,active,19,42,100,2000,kg",
|
||||
"Site 1,Location 1,Rack 5,active,19,42,100,2000,kg",
|
||||
"Site 2,Location 2,Rack 6,active,19,42,100,2000,kg",
|
||||
"site,location,group,name,status,width,u_height,weight,max_weight,weight_unit",
|
||||
"Site 1,,,Rack 4,active,19,42,100,2000,kg",
|
||||
"Site 1,Location 1,Rack Group 1,Rack 5,active,19,42,100,2000,kg",
|
||||
"Site 2,Location 2,Rack Group 2,Rack 6,active,19,42,100,2000,kg",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
|
@ -529,6 +577,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_DEPRECATED,
|
||||
'role': rackroles[1].pk,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class RackPanel(panels.ObjectAttributesPanel):
|
|||
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||
location = attrs.NestedObjectAttr('location', linkify=True)
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True, label=_('Rack group'))
|
||||
name = attrs.TextAttr('name')
|
||||
facility_id = attrs.TextAttr('facility_id', label=_('Facility ID'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ urlpatterns = [
|
|||
path('locations/', include(get_model_urls('dcim', 'location', detail=False))),
|
||||
path('locations/<int:pk>/', include(get_model_urls('dcim', 'location'))),
|
||||
|
||||
path('rack-groups/', include(get_model_urls('dcim', 'rackgroup', detail=False))),
|
||||
path('rack-groups/<int:pk>/', include(get_model_urls('dcim', 'rackgroup'))),
|
||||
|
||||
path('rack-roles/', include(get_model_urls('dcim', 'rackrole', detail=False))),
|
||||
path('rack-roles/<int:pk>/', include(get_model_urls('dcim', 'rackrole'))),
|
||||
|
||||
|
|
|
|||
|
|
@ -785,6 +785,85 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
|
|||
table = tables.LocationTable
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'list', path='', detail=False)
|
||||
class RackGroupListView(generic.ObjectListView):
|
||||
queryset = RackGroup.objects.annotate(
|
||||
rack_count=count_related(Rack, 'group')
|
||||
)
|
||||
filterset = filtersets.RackGroupFilterSet
|
||||
filterset_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
|
||||
|
||||
@register_model_view(RackGroup)
|
||||
class RackGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = RackGroup.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
OrganizationalObjectPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'add', detail=False)
|
||||
@register_model_view(RackGroup, 'edit')
|
||||
class RackGroupEditView(generic.ObjectEditView):
|
||||
queryset = RackGroup.objects.all()
|
||||
form = forms.RackGroupForm
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'delete')
|
||||
class RackGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = RackGroup.objects.all()
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'bulk_import', path='import', detail=False)
|
||||
class RackGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = RackGroup.objects.all()
|
||||
model_form = forms.RackGroupImportForm
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'bulk_edit', path='edit', detail=False)
|
||||
class RackGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = RackGroup.objects.annotate(
|
||||
rack_count=count_related(Rack, 'group')
|
||||
)
|
||||
filterset = filtersets.RackGroupFilterSet
|
||||
table = tables.RackGroupTable
|
||||
form = forms.RackGroupBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'bulk_rename', path='rename', detail=False)
|
||||
class RackGroupBulkRenameView(generic.BulkRenameView):
|
||||
queryset = RackGroup.objects.all()
|
||||
filterset = filtersets.RackGroupFilterSet
|
||||
|
||||
|
||||
@register_model_view(RackGroup, 'bulk_delete', path='delete', detail=False)
|
||||
class RackGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackGroup.objects.annotate(
|
||||
rack_count=count_related(Rack, 'group')
|
||||
)
|
||||
filterset = filtersets.RackGroupFilterSet
|
||||
table = tables.RackGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
|
@ -1160,7 +1239,7 @@ class RackReservationView(generic.ObjectView):
|
|||
queryset = RackReservation.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RackPanel(accessor='object.rack', only=['region', 'site', 'location', 'name']),
|
||||
panels.RackPanel(accessor='object.rack', only=['region', 'site', 'location', 'group', 'name']),
|
||||
panels.RackReservationPanel(title=_('Reservation')),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
|
|
|
|||
|
|
@ -1279,6 +1279,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
'provideraccount',
|
||||
'providernetwork',
|
||||
'rack',
|
||||
'rackgroup',
|
||||
'rackreservation',
|
||||
'rackrole',
|
||||
'racktype',
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ VLAN_VID_MAX = 4094
|
|||
|
||||
# models values for ContentTypes which may be VLANGroup scope types
|
||||
VLANGROUP_SCOPE_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
|
||||
'region', 'sitegroup', 'site', 'location', 'rackgroup', 'rack', 'clustergroup', 'cluster',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -958,6 +958,9 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||
location = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
rack_group = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
rack = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Location, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import Device, Location, Rack, RackGroup, Region, Site, SiteGroup
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.models import *
|
||||
|
|
@ -458,7 +458,7 @@ class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
|
|||
class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
|
||||
FieldSet('region', 'site_group', 'site', 'location', 'rack_group', 'rack', name=_('Location')),
|
||||
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
|
||||
FieldSet('contains_vid', name=_('VLANs')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
|
|
@ -485,6 +485,11 @@ class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
|||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
rack_group = DynamicModelMultipleChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Rack group')
|
||||
)
|
||||
rack = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ RACKS_MENU = Menu(
|
|||
label=_('Racks'),
|
||||
items=(
|
||||
get_model_item('dcim', 'rack', _('Racks')),
|
||||
get_model_item('dcim', 'rackrole', _('Rack Roles')),
|
||||
get_model_item('dcim', 'rackreservation', _('Reservations')),
|
||||
MenuItem(
|
||||
link='dcim:rack_elevation_list',
|
||||
|
|
@ -59,6 +58,13 @@ RACKS_MENU = Menu(
|
|||
),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Rack Organization'),
|
||||
items=(
|
||||
get_model_item('dcim', 'rackgroup', _('Rack Groups')),
|
||||
get_model_item('dcim', 'rackrole', _('Rack Roles')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Rack Types'),
|
||||
items=(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_rack %}
|
||||
<a href="{% url 'dcim:rack_add' %}?group={{ object.pk }}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Rack" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
Loading…
Reference in New Issue