Add rack-types (#3026)

* feat: add front image

* feat: startech 4post rack

* feat: add digitus DN-19 07U-I-OD

* feat: add digitus DN-19 07U-I-OD

* Revert "feat: add front image"

This reverts commit bdd27607f8.

* fix: change form_factor

* feat: add racktype schema

* feat: add racktype schema

* feat: upload generated known-racks.pickle

* fix: change filename

* feat: add rack type

* refactor: change tests to work with racks

* refactor: use the correct device class

* fix: rename file correctly

* Update racktype.json

* fix: add missing desc_units schema property

* fix: add missing desc_units value

* chore: update readme with rack-types documentation

---------

Co-authored-by: Harry <Harry@cadby.co.uk>
This commit is contained in:
Bastian Leicht
2025-08-11 14:07:27 +02:00
committed by GitHub
parent a34ad442f3
commit 57742dbb2c
9 changed files with 225 additions and 2 deletions

View File

@@ -103,6 +103,48 @@ The following fields may **optionally** be declared:
For further detail on these attributes and those listed below, please reference the
[schema definitions](schema/) and the [Component Definitions](#component-definitions) below.
## Rack Type Definitions
Each definition **must** include at minimum the following fields:
- `manufacturer`: The name of the manufacturer which produces this rack type.
- Type: String
- `model`: The model number of the rack type. This must be unique per manufacturer.
- Type: String
- `slug`: A URL-friendly representation of the model number. Like the model number, this must be unique per
manufacturer. All slugs should have the manufacturers name prepended to it with a dash, please see the example below.
- Type: String
- Pattern: `"^[-a-z0-9_]+$"`. Must match the following characters: `-`, Lowercase `a` to `z`, Numbers `0` to `9`.
- `form_factor`: The form factor of the rack type. This is used to indicate the physical characteristics of the rack, such as whether it is a 4-post frame or a wall-cabinet etc.
- Type: String
- :test_tube: Example: `form_factor: 4-post-frame`
- `width`: The width of the rack type in zoll/inches. This is used to indicate the physical width of the rack, such as whether it is a 19" or 23" rack.
- Type: Integer
- :test_tube: Example: `width: 19`
- `u_height`: The height of the rack type in rack units.
- Type: Number
- :test_tube: Example: `u_height: 42`
- `starting_unit`: The unit number at which the rack starts. This is used to indicate the starting unit number of the rack, such as whether it starts at 1 or 42. The starting unit is normally defined from bottom to top, with the bottom unit being 1.
- Type: Number
- :test_tube: Example: `starting_unit: 1`
:test_tube: Example:
```yaml
manufacturer: Startech
model: 4 Post 42U
slug: startech-4postrack42
form_factor: 4-post-frame
width: 19
u_height: 42
starting_unit: 1
```
**Note: We are asking that all new racks also include the following optional fields: `outer_width`, `outer_height`, `outer_depth`, `outer_unit`, `weight`, `max_weight`, `weight_unit`, `mounting_depth`, and `desc_units`.**
For further detail on these attributes and those listed below, please reference the
[racktype schema definition](schema/racktype.json)
### Component Definitions
Valid component types are listed below. Each type of component must declare a list of the individual component templates

View File

@@ -0,0 +1,16 @@
---
manufacturer: Digitus
model: DN-19 07U-I-OD
slug: digitus-dn-19-07u-i-od
width: 19
u_height: 7
form_factor: wall-cabinet
description: '[Datasheet](https://www.assmann.com/product-pdf/4016032360971?PL=de)'
starting_unit: 1
outer_width: 600
outer_unit: mm
mounting_depth: 450
weight: 31
max_weight: 100
weight_unit: kg
desc_units: false

View File

@@ -0,0 +1,24 @@
---
manufacturer: Startech
model: 4 Post 42U
slug: startech-4postrack42
width: 19
u_height: 42
form_factor: 4-post-frame
description: Startech 4 Post 42U 19in rack with optional casters
starting_unit: 1
outer_width: 600
outer_unit: mm
# Adjustable depth, do we want the minimum or maximum depth?
# Minimum adjusted depth
mounting_depth: 560
# Maximum adjusted depth
# mounting_depth: 1017
weight: 38.5
# Different weights between stationary and on casters, which one?
# Stationary
# max_weight: 600
# Rolling
max_weight: 360
weight_unit: kg
desc_units: false

100
schema/racktype.json Normal file
View File

@@ -0,0 +1,100 @@
{
"type": "object",
"$id": "urn:devicetype-library:rack-type",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"manufacturer": {
"type": "string"
},
"model": {
"type": "string"
},
"slug": {
"type": "string",
"pattern": "^[-a-z0-9_]+$"
},
"description": {
"type": "string"
},
"form_factor": {
"type": "string",
"enum": [
"wall-cabinet",
"4-post-frame",
"2-post-frame",
"4-post-cabinet",
"wall-frame",
"wall-frame-vertical",
"wall-cabinet-vertical"
]
},
"width": {
"type": "integer",
"enum": [
10,
19,
20,
23
]
},
"u_height": {
"type": "number",
"minimum": 0,
"multipleOf": 1
},
"outer_width": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"outer_height": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"outer_depth": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"outer_unit": {
"type": "string",
"enum": [
"mm",
"in"
]
},
"weight": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"max_weight": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"weight_unit": {
"$ref": "urn:devicetype-library:generated-schema#/definitions/weight-unit"
},
"mounting_depth": {
"type": "number",
"minimum": 0,
"multipleOf": 0.01
},
"starting_unit": {
"type": "number",
"minimum": 1,
"multipleOf": 1
},
"desc_units": {
"type": "boolean",
"default": false
},
"comments": {
"type": "string"
}
},
"required": ["manufacturer", "model", "slug", "form_factor", "width", "u_height", "starting_unit"],
"additionalProperties": false
}

View File

@@ -1,7 +1,7 @@
from test_configuration import COMPONENT_TYPES, IMAGE_FILETYPES, SCHEMAS, SCHEMAS_BASEPATH, KNOWN_SLUGS, ROOT_DIR, USE_LOCAL_KNOWN_SLUGS, NETBOX_DT_LIBRARY_URL, KNOWN_MODULES, USE_UPSTREAM_DIFF, PRECOMMIT_ALL_SWITCHES
import pickle_operations
from yaml_loader import DecimalSafeLoader
from device_types import DeviceType, ModuleType, verify_filename, validate_components
from device_types import DeviceType, ModuleType, RackType, verify_filename, validate_components
import decimal
import glob
import json
@@ -134,11 +134,13 @@ image_files = _get_image_files()
if USE_LOCAL_KNOWN_SLUGS:
KNOWN_SLUGS = pickle_operations.read_pickle_data(f'{ROOT_DIR}/tests/known-slugs.pickle')
KNOWN_MODULES = pickle_operations.read_pickle_data(f'{ROOT_DIR}/tests/known-modules.pickle')
KNOWN_RACKS = pickle_operations.read_pickle_data(f'{ROOT_DIR}/tests/known-racks.pickle')
else:
temp_dir = tempfile.TemporaryDirectory()
repo = Repo.clone_from(url=NETBOX_DT_LIBRARY_URL, to_path=temp_dir.name)
KNOWN_SLUGS = pickle_operations.read_pickle_data(f'{temp_dir.name}/tests/known-slugs.pickle')
KNOWN_MODULES = pickle_operations.read_pickle_data(f'{temp_dir.name}/tests/known-modules.pickle')
KNOWN_RACKS = pickle_operations.read_pickle_data(f'{ROOT_DIR}/tests/known-racks.pickle')
SCHEMA_REGISTRY = _generate_schema_registry()
@@ -181,6 +183,12 @@ def test_definitions(file_path, schema, change_type):
if "device-types" in file_path:
# A device
this_device = DeviceType(definition, file_path, change_type)
elif "module-types" in file_path:
# A module type
this_device = ModuleType(definition, file_path, change_type)
elif "rack-types" in file_path:
# A rack type
this_device = RackType(definition, file_path, change_type)
else:
# A module
this_device = ModuleType(definition, file_path, change_type)

View File

@@ -179,6 +179,28 @@ class ModuleType:
slugified = slugified[:-1]
return slugified
class RackType:
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
def __init__(self, definition, file_path, change_type):
self.file_path = file_path
self.isDevice = False
self.definition = definition
self.manufacturer = definition.get('manufacturer')
self.model = definition.get('model')
self._slug_model = self._slugify_model()
self.change_type = change_type
def get_filepath(self):
return self.file_path
def _slugify_model(self):
slugified = self.model.casefold().replace(" ", "-").replace("sfp+", "sfpp").replace("poe+", "poep").replace("-+", "-plus").replace("+", "-plus-").replace("_", "-").replace("&", "-and-").replace("!", "").replace("/", "-").replace(",", "").replace("'", "").replace("*", "-")
if slugified.endswith("-"):
slugified = slugified[:-1]
return slugified
def validate_component_names(component_names: (set or None)):
if len(component_names) > 1:
verify_name = list(component_names[0])
@@ -197,10 +219,17 @@ def validate_component_names(component_names: (set or None)):
return False
return True
def verify_filename(device: (DeviceType or ModuleType), KNOWN_MODULES: (set or None)):
def verify_filename(device: (DeviceType or ModuleType or RackType), KNOWN_MODULES: (set or None)):
head, tail = os.path.split(device.get_filepath())
filename = tail.rsplit(".", 1)[0].casefold()
# Check if file is RackType
if "rack-types" in device.file_path:
if not filename == device._slug_model:
device.failureMessage = f'{device.file_path} file name is invalid. Must be the model "{device._slug_model}"'
return False
return True
if not (filename == device._slug_model or filename == device._slug_part_number or filename == device.part_number.casefold()):
device.failureMessage = f'{device.file_path} file name is invalid. Must be either the model "{device._slug_model}" or part_number "{device.part_number} / {device._slug_part_number}"'
return False

View File

@@ -107,3 +107,6 @@ pickle_operations.write_pickle_data(KNOWN_SLUGS, f'{ROOT_DIR}/tests/known-slugs.
_generate_knowns('module')
pickle_operations.write_pickle_data(KNOWN_MODULES, f'{ROOT_DIR}/tests/known-modules.pickle')
_generate_knowns('rack')
pickle_operations.write_pickle_data(KNOWN_MODULES, f'{ROOT_DIR}/tests/known-racks.pickle')

BIN
tests/known-racks.pickle Normal file

Binary file not shown.

View File

@@ -3,6 +3,7 @@ import os
SCHEMAS = (
('device-types', 'devicetype.json'),
('module-types', 'moduletype.json'),
('rack-types', 'racktype.json'),
)
SCHEMAS_BASEPATH = f"{os.getcwd()}/schema/"