Add documentation & tests

This commit is contained in:
Jeremy Stretch 2025-03-28 13:08:26 -04:00
parent 94b3aae0a2
commit 7edc67ed8a
6 changed files with 311 additions and 4 deletions

View File

@ -43,3 +43,11 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
### Airflow
The direction in which air circulates through the device chassis for cooling.
### Profile
The assigned [profile](./moduletypeprofile.md) for the type of module. Profiles can be used to classify module types by function (e.g. power supply, hard disk, etc.), and they support the addition of user-configurable attributes on module types. The assignment of a module type to a profile is optional.
### Attributes
Depending on the module type's assigned [profile](./moduletypeprofile.md) (if any), one or more user-defined attributes may be available to configure.

View File

@ -0,0 +1,40 @@
# Module Type Profiles
!!! info "This model was introduced in NetBox v4.3."
Each [module type](./moduletype.md) may optionally be assigned a profile according to its classification. A profile can extend module types with user-configured attributes. For example, you might want to specify the input current and voltage of a power supply, or the clock speed and number of cores for a processor.
Module type attributes are managed via the configuration of a [JSON schema](https://json-schema.org/) on the profile. For example, the following schema introduces three module type attributes, two of which are designated as required attributes.
```json
{
"properties": {
"type": {
"type": "string",
"title": "Disk type",
"enum": ["HD", "SSD", "NVME"],
"default": "HD"
},
"capacity": {
"type": "integer",
"title": "Capacity (GB)",
"description": "Gross disk size"
},
"speed": {
"type": "integer",
"title": "Speed (RPM)"
}
},
"required": [
"type", "capacity"
]
}
```
The assignment of module types to a profile is optional. The designation of a schema for a profile is also optional: A profile can be used simply as a mechanism for classifying module types if the addition of custom attributes is not needed.
## Fields
### Schema
This field holds the [JSON schema](https://json-schema.org/) for the profile. The configured JSON schema must be valid (or the field must be null).

View File

@ -601,7 +601,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType):
pagination=True
)
class ModuleTypeProfileType(NetBoxObjectType):
moduletypes: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(

View File

@ -629,6 +629,70 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
]
class ModuleTypeProfileTest(APIViewTestCases.APIViewTestCase):
model = ModuleTypeProfile
brief_fields = ['description', 'display', 'id', 'name', 'url']
SCHEMAS = [
{
"properties": {
"foo": {
"type": "string"
}
}
},
{
"properties": {
"foo": {
"type": "integer"
}
}
},
{
"properties": {
"foo": {
"type": "boolean"
}
}
},
]
create_data = [
{
'name': 'Module Type Profile 4',
'schema': SCHEMAS[0],
},
{
'name': 'Module Type Profile 5',
'schema': SCHEMAS[1],
},
{
'name': 'Module Type Profile 6',
'schema': SCHEMAS[2],
},
]
bulk_update_data = {
'description': 'New description',
'comments': 'New comments',
}
@classmethod
def setUpTestData(cls):
module_type_profiles = (
ModuleTypeProfile(
name='Module Type Profile 1',
schema=cls.SCHEMAS[0]
),
ModuleTypeProfile(
name='Module Type Profile 2',
schema=cls.SCHEMAS[1]
),
ModuleTypeProfile(
name='Module Type Profile 3',
schema=cls.SCHEMAS[2]
),
)
ModuleTypeProfile.objects.bulk_create(module_type_profiles)
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsolePortTemplate
brief_fields = ['description', 'display', 'id', 'name', 'url']

View File

@ -1488,6 +1488,15 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
filterset = ModuleTypeFilterSet
ignore_fields = ['attribute_data']
PROFILE_SCHEMA = {
"properties": {
"string": {"type": "string"},
"integer": {"type": "integer"},
"number": {"type": "number"},
"boolean": {"type": "boolean"},
}
}
@classmethod
def setUpTestData(cls):
@ -1497,6 +1506,21 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
module_type_profiles = (
ModuleTypeProfile(
name='Module Type Profile 1',
schema=cls.PROFILE_SCHEMA
),
ModuleTypeProfile(
name='Module Type Profile 2',
schema=cls.PROFILE_SCHEMA
),
ModuleTypeProfile(
name='Module Type Profile 3',
schema=cls.PROFILE_SCHEMA
),
)
ModuleTypeProfile.objects.bulk_create(module_type_profiles)
module_types = (
ModuleType(
@ -1506,7 +1530,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
weight=10,
weight_unit=WeightUnitChoices.UNIT_POUND,
description='foobar1',
airflow=ModuleAirflowChoices.FRONT_TO_REAR
airflow=ModuleAirflowChoices.FRONT_TO_REAR,
profile=module_type_profiles[0],
attribute_data={
'string': 'string1',
'integer': 1,
'number': 1.0,
'boolean': True,
}
),
ModuleType(
manufacturer=manufacturers[1],
@ -1515,7 +1546,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
weight=20,
weight_unit=WeightUnitChoices.UNIT_POUND,
description='foobar2',
airflow=ModuleAirflowChoices.REAR_TO_FRONT
airflow=ModuleAirflowChoices.REAR_TO_FRONT,
profile=module_type_profiles[1],
attribute_data={
'string': 'string2',
'integer': 2,
'number': 2.0,
'boolean_': False,
}
),
ModuleType(
manufacturer=manufacturers[2],
@ -1523,7 +1561,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
part_number='Part Number 3',
weight=30,
weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
description='foobar3'
description='foobar3',
profile=module_type_profiles[2],
attribute_data={
'string': 'string3',
'integer': 3,
'number': 3.0,
'boolean': None,
}
),
)
ModuleType.objects.bulk_create(module_types)
@ -1642,6 +1687,82 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'airflow': RackAirflowChoices.FRONT_TO_REAR}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_profile(self):
profiles = ModuleTypeProfile.objects.all()[:2]
params = {'profile_id': [profiles[0].pk, profiles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'profile': [profiles[0].name, profiles[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_profile_attributes(self):
params = {'attr_string': 'string1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'attr_integer': '1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'attr_number': '2.0'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'attr_boolean': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ModuleTypeProfileTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleTypeProfile.objects.all()
filterset = ModuleTypeProfileFilterSet
ignore_fields = ['schema']
SCHEMAS = [
{
"properties": {
"foo": {
"type": "string"
}
}
},
{
"properties": {
"foo": {
"type": "integer"
}
}
},
{
"properties": {
"foo": {
"type": "boolean"
}
}
},
]
@classmethod
def setUpTestData(cls):
module_type_profiles = (
ModuleTypeProfile(
name='Module Type Profile 1',
description='foobar1',
schema=cls.SCHEMAS[0]
),
ModuleTypeProfile(
name='Module Type Profile 2',
description='foobar2 2',
schema=cls.SCHEMAS[1]
),
ModuleTypeProfile(
name='Module Type Profile 3',
description='foobar3',
schema=cls.SCHEMAS[2]
),
)
ModuleTypeProfile.objects.bulk_create(module_type_profiles)
def test_q(self):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Module Type Profile 1', 'Module Type Profile 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ConsolePortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()

View File

@ -1,3 +1,4 @@
import json
from decimal import Decimal
from zoneinfo import ZoneInfo
@ -1305,6 +1306,79 @@ front-ports:
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
class ModuleTypeProfileTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = ModuleTypeProfile
SCHEMAS = [
{
"properties": {
"foo": {
"type": "string"
}
}
},
{
"properties": {
"foo": {
"type": "integer"
}
}
},
{
"properties": {
"foo": {
"type": "boolean"
}
}
},
]
@classmethod
def setUpTestData(cls):
module_type_profiles = (
ModuleTypeProfile(
name='Module Type Profile 1',
schema=cls.SCHEMAS[0]
),
ModuleTypeProfile(
name='Module Type Profile 2',
schema=cls.SCHEMAS[1]
),
ModuleTypeProfile(
name='Module Type Profile 3',
schema=cls.SCHEMAS[2]
),
)
ModuleTypeProfile.objects.bulk_create(module_type_profiles)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Module Type Profile X',
'description': 'A new profile',
'schema': json.dumps(cls.SCHEMAS[0]),
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,schema",
f"Module Type Profile 4,{json.dumps(cls.SCHEMAS[0])}",
f"Module Type Profile 5,{json.dumps(cls.SCHEMAS[1])}",
f"Module Type Profile 6,{json.dumps(cls.SCHEMAS[2])}",
)
cls.csv_update_data = (
"id,description",
f"{module_type_profiles[0].pk},New description",
f"{module_type_profiles[1].pk},New description",
f"{module_type_profiles[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
}
#
# DeviceType components
#