From 2b640b6aba2bf8ce5a4405d19aae2c45666a469e Mon Sep 17 00:00:00 2001 From: Xinhao Luo Date: Tue, 31 Mar 2026 18:51:09 -0700 Subject: [PATCH 1/3] Add platform schema, default_platform field, PlatformType class, and test config - Create schema/platformtype.json for platform definitions - Add optional default_platform field to device type schema - Add PlatformType class to tests/device_types.py - Register platforms in SCHEMAS tuple in test_configuration.py Co-Authored-By: Claude Opus 4.6 (1M context) --- schema/devicetype.json | 5 +++++ schema/platformtype.json | 26 ++++++++++++++++++++++++++ tests/device_types.py | 16 ++++++++++++++++ tests/test_configuration.py | 1 + 4 files changed, 48 insertions(+) create mode 100644 schema/platformtype.json diff --git a/schema/devicetype.json b/schema/devicetype.json index 9f8ef3ea1a..fa77347f9c 100644 --- a/schema/devicetype.json +++ b/schema/devicetype.json @@ -115,6 +115,11 @@ }, "comments": { "type": "string" + }, + "default_platform": { + "type": "string", + "maxLength": 100, + "pattern": "^[-a-zA-Z0-9_]+$" } }, "allOf": [ diff --git a/schema/platformtype.json b/schema/platformtype.json new file mode 100644 index 0000000000..7d483ff9f0 --- /dev/null +++ b/schema/platformtype.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "$id": "urn:devicetype-library:platform-type", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "manufacturer": { + "type": "string", + "maxLength": 100 + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "slug": { + "type": "string", + "maxLength": 100, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": ["manufacturer", "name", "slug"], + "additionalProperties": false +} diff --git a/tests/device_types.py b/tests/device_types.py index 0d967fd8ab..b7484d5999 100644 --- a/tests/device_types.py +++ b/tests/device_types.py @@ -208,6 +208,22 @@ def _slugify_model(self): slugified = slugified[:-1] return slugified +class PlatformType: + 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.name = definition.get('name') + self.slug = definition.get('slug') + self.change_type = change_type + + def get_filepath(self): + return self.file_path + def validate_component_names(component_names: (set or None)): if len(component_names) > 1: verify_name = list(component_names[0]) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4c1e9bb3c0..7a615396f8 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -4,6 +4,7 @@ ('device-types', 'devicetype.json'), ('module-types', 'moduletype.json'), ('rack-types', 'racktype.json'), + ('platforms', 'platformtype.json'), ) SCHEMAS_BASEPATH = f"{os.getcwd()}/schema/" From d7236c7219964b15de8d2363349bee3d45153e09 Mon Sep 17 00:00:00 2001 From: Xinhao Luo Date: Tue, 31 Mar 2026 18:54:12 -0700 Subject: [PATCH 2/3] Add platform test validations and seed platform definitions - Route platform files to PlatformType in test_definitions - Validate platform filename matches slug field - Cross-validate default_platform references existing platform file - Skip device-only validations (verify_filename, validate_components) for platforms - Add seed platforms: Arista EOS, Cisco IOS-XE, Cisco NX-OS, Juniper Junos, PAN-OS Co-Authored-By: Claude Opus 4.6 (1M context) --- platforms/Arista/arista-eos.yaml | 5 ++++ platforms/Cisco/cisco-ios-xe.yaml | 5 ++++ platforms/Cisco/cisco-nxos.yaml | 5 ++++ platforms/Juniper/juniper-junos.yaml | 5 ++++ platforms/Palo Alto/paloalto-panos.yaml | 5 ++++ tests/definitions_test.py | 36 ++++++++++++++++++++++--- 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 platforms/Arista/arista-eos.yaml create mode 100644 platforms/Cisco/cisco-ios-xe.yaml create mode 100644 platforms/Cisco/cisco-nxos.yaml create mode 100644 platforms/Juniper/juniper-junos.yaml create mode 100644 platforms/Palo Alto/paloalto-panos.yaml diff --git a/platforms/Arista/arista-eos.yaml b/platforms/Arista/arista-eos.yaml new file mode 100644 index 0000000000..13e6e6a3dc --- /dev/null +++ b/platforms/Arista/arista-eos.yaml @@ -0,0 +1,5 @@ +--- +manufacturer: Arista +name: Arista EOS +slug: arista-eos +description: Arista Extensible Operating System diff --git a/platforms/Cisco/cisco-ios-xe.yaml b/platforms/Cisco/cisco-ios-xe.yaml new file mode 100644 index 0000000000..5e06e979f1 --- /dev/null +++ b/platforms/Cisco/cisco-ios-xe.yaml @@ -0,0 +1,5 @@ +--- +manufacturer: Cisco +name: Cisco IOS-XE +slug: cisco-ios-xe +description: Cisco Internet Operating System XE diff --git a/platforms/Cisco/cisco-nxos.yaml b/platforms/Cisco/cisco-nxos.yaml new file mode 100644 index 0000000000..c2513220cc --- /dev/null +++ b/platforms/Cisco/cisco-nxos.yaml @@ -0,0 +1,5 @@ +--- +manufacturer: Cisco +name: Cisco NX-OS +slug: cisco-nxos +description: Cisco Nexus Operating System diff --git a/platforms/Juniper/juniper-junos.yaml b/platforms/Juniper/juniper-junos.yaml new file mode 100644 index 0000000000..a69f54a71e --- /dev/null +++ b/platforms/Juniper/juniper-junos.yaml @@ -0,0 +1,5 @@ +--- +manufacturer: Juniper +name: Juniper Junos +slug: juniper-junos +description: Juniper Networks Junos Operating System diff --git a/platforms/Palo Alto/paloalto-panos.yaml b/platforms/Palo Alto/paloalto-panos.yaml new file mode 100644 index 0000000000..babcea8c9c --- /dev/null +++ b/platforms/Palo Alto/paloalto-panos.yaml @@ -0,0 +1,5 @@ +--- +manufacturer: Palo Alto +name: Palo Alto PAN-OS +slug: paloalto-panos +description: Palo Alto Networks PAN-OS diff --git a/tests/definitions_test.py b/tests/definitions_test.py index 56a6d6010a..b18588dd07 100644 --- a/tests/definitions_test.py +++ b/tests/definitions_test.py @@ -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, RackType, verify_filename, validate_components +from device_types import DeviceType, ModuleType, RackType, PlatformType, verify_filename, validate_components import decimal import glob import json @@ -188,7 +188,7 @@ def test_definitions(file_path, schema, change_type): # Schema validation failure. Ensure you are following the proper format. pytest.fail(f"{file_path} failed validation: {e}", False) - # Identify if the definition is for a Device or Module + # Identify if the definition is for a Device, Module, Rack, or Platform if "device-types" in file_path: # A device this_device = DeviceType(definition, file_path, change_type) @@ -198,10 +198,22 @@ def test_definitions(file_path, schema, change_type): elif "rack-types" in file_path: # A rack type this_device = RackType(definition, file_path, change_type) + elif "platforms" in file_path: + # A platform + this_device = PlatformType(definition, file_path, change_type) else: # A module this_device = ModuleType(definition, file_path, change_type) + # Validate platform filename matches slug + if "platforms" in file_path: + platform_filename = os.path.basename(file_path).rsplit(".", 1)[0] + if platform_filename != definition.get('slug'): + pytest.fail( + f"{file_path}: filename '{platform_filename}' does not match slug '{definition.get('slug')}'", + pytrace=False, + ) + # Validate that front-ports reference existing rear-ports if any(x in file_path for x in ("device-types", "module-types")): rear_ports = definition.get("rear-ports", []) or [] @@ -244,10 +256,12 @@ def test_definitions(file_path, schema, change_type): assert this_device.verify_slug(KNOWN_SLUGS), pytest.fail(this_device.failureMessage, False) # Verify the filename is valid. Must either be the model or part_number. - assert verify_filename(this_device, (KNOWN_MODULES if not this_device.isDevice else None)), pytest.fail(this_device.failureMessage, False) + if not isinstance(this_device, PlatformType): + assert verify_filename(this_device, (KNOWN_MODULES if not this_device.isDevice else None)), pytest.fail(this_device.failureMessage, False) # Check for duplicate components within the definition - assert validate_components(COMPONENT_TYPES, this_device), pytest.fail(this_device.failureMessage, False) + if not isinstance(this_device, PlatformType): + assert validate_components(COMPONENT_TYPES, this_device), pytest.fail(this_device.failureMessage, False) # Check for empty quotes and fail if found def iterdict(var): @@ -295,4 +309,18 @@ def iterlist(var): if not rear_image: pytest.fail(f'{file_path} has rear_image set to True but no matching image found (looking for \'elevation-images{os.path.sep}{file_path.split(os.path.sep)[1]}{os.path.sep}{this_device.get_slug()}.rear.ext\' but only found {manufacturer_images})', False) + + # Validate default_platform references an existing platform file + if "device-types" in file_path and definition.get('default_platform'): + platform_slug = definition['default_platform'] + matching_platforms = glob.glob(f"platforms{os.path.sep}*{os.path.sep}{platform_slug}.yaml") + if not matching_platforms: + matching_platforms = glob.glob(f"platforms{os.path.sep}*{os.path.sep}{platform_slug}.yml") + if not matching_platforms: + pytest.fail( + f"{file_path} has default_platform '{platform_slug}' but no matching platform definition " + f"was found in platforms/*/. Expected file: platforms//{platform_slug}.yaml", + pytrace=False, + ) + iterdict(definition) From 9afe62734ae73e82f2cfcf95b56e8ba6a0938e0c Mon Sep 17 00:00:00 2001 From: Xinhao Luo Date: Tue, 31 Mar 2026 22:30:23 -0700 Subject: [PATCH 3/3] Address Copilot review: lowercase slug pattern, precompute platforms, duplicate detection - Align slug patterns to lowercase-only (^[-a-z0-9_]+$) matching repo convention - Precompute KNOWN_PLATFORMS map at module level instead of glob per device - Fail if multiple platform files share the same slug across manufacturers Co-Authored-By: Claude Opus 4.6 (1M context) --- schema/devicetype.json | 2 +- schema/platformtype.json | 2 +- tests/definitions_test.py | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/schema/devicetype.json b/schema/devicetype.json index fa77347f9c..a6a1d5d87f 100644 --- a/schema/devicetype.json +++ b/schema/devicetype.json @@ -119,7 +119,7 @@ "default_platform": { "type": "string", "maxLength": 100, - "pattern": "^[-a-zA-Z0-9_]+$" + "pattern": "^[-a-z0-9_]+$" } }, "allOf": [ diff --git a/schema/platformtype.json b/schema/platformtype.json index 7d483ff9f0..d542555618 100644 --- a/schema/platformtype.json +++ b/schema/platformtype.json @@ -14,7 +14,7 @@ "slug": { "type": "string", "maxLength": 100, - "pattern": "^[-a-zA-Z0-9_]+$" + "pattern": "^[-a-z0-9_]+$" }, "description": { "type": "string", diff --git a/tests/definitions_test.py b/tests/definitions_test.py index b18588dd07..589df66de1 100644 --- a/tests/definitions_test.py +++ b/tests/definitions_test.py @@ -131,6 +131,12 @@ def test_environment(): definition_files = _get_definition_files() image_files = _get_image_files() +# Precompute known platform slugs for default_platform cross-validation +KNOWN_PLATFORMS = {} +for platform_file in sorted(glob.glob(f"platforms{os.path.sep}*{os.path.sep}*.yaml")) + sorted(glob.glob(f"platforms{os.path.sep}*{os.path.sep}*.yml")): + platform_slug = os.path.basename(platform_file).rsplit(".", 1)[0] + KNOWN_PLATFORMS.setdefault(platform_slug, []).append(platform_file) + 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') @@ -313,14 +319,18 @@ def iterlist(var): # Validate default_platform references an existing platform file if "device-types" in file_path and definition.get('default_platform'): platform_slug = definition['default_platform'] - matching_platforms = glob.glob(f"platforms{os.path.sep}*{os.path.sep}{platform_slug}.yaml") - if not matching_platforms: - matching_platforms = glob.glob(f"platforms{os.path.sep}*{os.path.sep}{platform_slug}.yml") + matching_platforms = KNOWN_PLATFORMS.get(platform_slug, []) if not matching_platforms: pytest.fail( f"{file_path} has default_platform '{platform_slug}' but no matching platform definition " f"was found in platforms/*/. Expected file: platforms//{platform_slug}.yaml", pytrace=False, ) + elif len(matching_platforms) > 1: + pytest.fail( + f"{file_path} has default_platform '{platform_slug}' but multiple matching platform definitions " + f"were found: {matching_platforms}. Platform slugs must be globally unique.", + pytrace=False, + ) iterdict(definition)