From 78c24657c976ba93689491d659b380d85b40a186 Mon Sep 17 00:00:00 2001 From: Andrey Golovanov Date: Thu, 7 Aug 2025 00:15:44 +0100 Subject: [PATCH] Add vars section to YAML schema and documentation --- docs/reference/api-full.md | 2 +- docs/reference/dsl.md | 32 +++++++++++++ ngraph/scenario.py | 3 ++ schemas/scenario.json | 4 ++ tests/test_scenario.py | 84 +++++++++++++++++++++++++++++++++ tests/test_schema_validation.py | 13 +++++ 6 files changed, 137 insertions(+), 1 deletion(-) diff --git a/docs/reference/api-full.md b/docs/reference/api-full.md index b6a60fd..fb759f0 100644 --- a/docs/reference/api-full.md +++ b/docs/reference/api-full.md @@ -10,7 +10,7 @@ For a curated, example-driven API guide, see **[api.md](api.md)**. > - **[CLI Reference](cli.md)** - Command-line interface > - **[DSL Reference](dsl.md)** - YAML syntax guide -**Generated from source code on:** August 06, 2025 at 21:33 UTC +**Generated from source code on:** August 07, 2025 at 00:14 UTC **Modules auto-discovered:** 53 diff --git a/docs/reference/dsl.md b/docs/reference/dsl.md index 8fff087..03fdec4 100644 --- a/docs/reference/dsl.md +++ b/docs/reference/dsl.md @@ -16,6 +16,7 @@ The scenario YAML file is organized around a **core foundation** that defines yo The main sections of a scenario YAML file work together to define a complete network simulation: +- `vars`: **[Optional]** Defines YAML anchors and variables for reuse throughout the scenario file. - `network`: **[Required]** Describes the actual network topology - nodes, links, and their connections. - `blueprints`: **[Optional]** Defines reusable network templates that can be instantiated multiple times within the network. - `components`: **[Optional]** A library of hardware and optics definitions with attributes like power consumption. @@ -442,6 +443,37 @@ failure_policy_set: - If only one policy exists and no `default` is specified, that policy becomes the default - Multiple policies allow testing different failure scenarios in the same network +## `vars` - YAML Anchors and Variables + +The `vars` section provides a designated space for YAML anchor definitions. YAML anchors (`&name`) and aliases (`*name`) follow the YAML 1.1 specification and are processed by PyYAML during parsing, before NetGraph validation. + +**Anchor Types:** + +- **Scalar anchors**: Reference primitive values (strings, numbers, booleans) +- **Sequence anchors**: Reference arrays/lists +- **Mapping anchors**: Reference objects/dictionaries +- **Merge keys (`<<`)**: Merge mapping properties with override capability + +**Minimal Example:** + +```yaml +vars: + default_cap: &cap 10000 + base_attrs: &attrs {cost: 100, region: "dc1"} + +network: + nodes: + N1: {attrs: {<<: *attrs, capacity: *cap}} + N2: {attrs: {<<: *attrs, capacity: *cap, region: "dc2"}} +``` + +**Processing Behavior:** + +- Anchors are resolved during YAML parsing, before schema validation +- The `vars` section itself is ignored by NetGraph runtime logic +- Anchors can be defined in any section, not just `vars` +- Merge operations follow YAML 1.1 semantics (later keys override earlier ones) + ## `workflow` - Execution Steps A list of operations to perform on the network. Each step has a `step_type` and specific arguments. This section defines the analysis workflow to be executed. diff --git a/ngraph/scenario.py b/ngraph/scenario.py index ad9ca34..03fd0f7 100644 --- a/ngraph/scenario.py +++ b/ngraph/scenario.py @@ -79,6 +79,7 @@ def from_yaml( with a default ComponentsLibrary if provided. Top-level YAML keys can include: + - vars - blueprints - network - failure_policy_set @@ -92,6 +93,7 @@ def from_yaml( If 'failure_policy_set' is omitted, scenario.failure_policy_set is empty. If 'components' is provided, it is merged with default_components. If 'seed' is provided, it enables reproducible random operations. + If 'vars' is provided, it can contain YAML anchors and aliases for reuse. If any unrecognized top-level key is found, a ValueError is raised. Args: @@ -115,6 +117,7 @@ def from_yaml( # Ensure only recognized top-level keys are present. recognized_keys = { + "vars", "blueprints", "network", "failure_policy_set", diff --git a/schemas/scenario.json b/schemas/scenario.json index 94e3d3e..f6a12e2 100644 --- a/schemas/scenario.json +++ b/schemas/scenario.json @@ -5,6 +5,10 @@ "description": "JSON Schema for NetGraph network scenario YAML files", "type": "object", "properties": { + "vars": { + "type": "object", + "description": "YAML anchors and variable definitions for reuse throughout the scenario" + }, "seed": { "type": "integer", "description": "Master seed for reproducible random operations across the scenario" diff --git a/tests/test_scenario.py b/tests/test_scenario.py index 9a49566..ff8221a 100644 --- a/tests/test_scenario.py +++ b/tests/test_scenario.py @@ -645,3 +645,87 @@ def test_failure_policy_docstring_yaml_full_scenario_integration(): # N2, N3 should not fail (different electric grids) assert "N2" not in failed assert "N3" not in failed + + +def test_yaml_anchors_and_aliases(): + """Test that YAML anchors and aliases work correctly with the vars section.""" + scenario_yaml = """ +vars: + default_capacity: &default_cap 100 + common_attrs: &common_attrs + region: "datacenter1" + type: "switch" + +network: + name: "anchor_test" + nodes: + N1: + attrs: + <<: *common_attrs + capacity: *default_cap + N2: + attrs: + <<: *common_attrs + capacity: *default_cap + type: "router" # Override the type + links: + - source: N1 + target: N2 + link_params: + capacity: *default_cap + +traffic_matrix_set: + default: [] +""" + + # Should load without errors + scenario = Scenario.from_yaml(scenario_yaml) + + # Verify the anchors were properly expanded + n1_attrs = scenario.network.nodes["N1"].attrs + n2_attrs = scenario.network.nodes["N2"].attrs + + # Both nodes should have the common attributes + assert n1_attrs["region"] == "datacenter1" + assert n2_attrs["region"] == "datacenter1" + assert n1_attrs["capacity"] == 100 + assert n2_attrs["capacity"] == 100 + + # N1 should have the original type from the anchor + assert n1_attrs["type"] == "switch" + # N2 should have the overridden type + assert n2_attrs["type"] == "router" + + # The link should also use the anchored capacity + link = list(scenario.network.links.values())[0] + assert link.capacity == 100 + + +def test_yaml_anchors_without_vars_section(): + """Test that YAML anchors work even without an explicit vars section.""" + scenario_yaml = """ +network: + name: "anchor_test" + nodes: + N1: &common_node + attrs: + region: "datacenter1" + type: "switch" + N2: + <<: *common_node + attrs: + region: "datacenter2" + type: "router" + links: [] + +traffic_matrix_set: + default: [] +""" + + # Should load without errors + scenario = Scenario.from_yaml(scenario_yaml) + + # Verify the network was created correctly + assert len(scenario.network.nodes) == 2 + assert "N1" in scenario.network.nodes + assert "N2" in scenario.network.nodes diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py index d272615..ea76fa1 100644 --- a/tests/test_schema_validation.py +++ b/tests/test_schema_validation.py @@ -124,6 +124,19 @@ def test_schema_requires_risk_group_name(self, schema): with pytest.raises(jsonschema.ValidationError): jsonschema.validate(invalid_data, schema) + def test_schema_validates_vars_section(self, schema): + """Test that the schema validates the vars section for YAML anchors.""" + valid_data = { + "vars": { + "default_capacity": 100, + "common_attrs": {"region": "datacenter1", "type": "switch"}, + }, + "network": {"nodes": {}, "links": []}, + } + + # Should not raise any validation errors + jsonschema.validate(valid_data, schema) + def test_schema_validates_link_risk_groups(self, schema): """Test that the schema validates risk_groups in link_params.""" valid_data = {