@@ -130,6 +130,11 @@ class LinkParams:
130130 cost : int
131131 attrs : dict [str , Any ] = field (default_factory = dict )
132132 match : dict [str , Any ] = field (default_factory = dict )
133+ # Unordered role-pairs allowed for this link type. Examples:
134+ # ["core|core", "core|leaf"] or [["core", "core"], ["core", "leaf"]]
135+ # The pipeline will auto-render a symmetric match from the union of roles.
136+ role_pairs : list [Any ] = field (default_factory = list )
137+ # endpoint_roles removed: explicit direction should not be configured.
133138
134139
135140@dataclass
@@ -280,6 +285,11 @@ class ComponentsConfig:
280285 """
281286
282287 assignments : ComponentAssignments = field (default_factory = ComponentAssignments )
288+ # New streamlined mappings (preferred over assignments when provided)
289+ # hw_component: role -> platform component name
290+ # optics: "srcRole-dstRole" -> optic component name (applies to source end)
291+ hw_component : dict [str , str ] = field (default_factory = dict )
292+ optics : dict [str , str ] = field (default_factory = dict )
283293
284294
285295@dataclass
@@ -653,6 +663,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
653663 ** intra_metro_link_dict .get ("attrs" , {}),
654664 },
655665 match = intra_metro_link_dict .get ("match" , {}),
666+ role_pairs = intra_metro_link_dict .get ("role_pairs" , []) or [],
656667 )
657668 inter_metro_link = LinkParams (
658669 capacity = inter_metro_link_dict .get ("capacity" , 100 ),
@@ -662,6 +673,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
662673 ** inter_metro_link_dict .get ("attrs" , {}),
663674 },
664675 match = inter_metro_link_dict .get ("match" , {}),
676+ role_pairs = inter_metro_link_dict .get ("role_pairs" , []) or [],
665677 )
666678 dc_to_pop_link = LinkParams (
667679 capacity = dc_to_pop_link_dict .get ("capacity" , 400 ),
@@ -671,6 +683,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
671683 ** dc_to_pop_link_dict .get ("attrs" , {}),
672684 },
673685 match = dc_to_pop_link_dict .get ("match" , {}),
686+ role_pairs = dc_to_pop_link_dict .get ("role_pairs" , []) or [],
674687 )
675688
676689 # Create BuildDefaults with explicit parameters
@@ -783,7 +796,7 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
783796 tm_sizing = tm_sizing ,
784797 )
785798
786- # Handle optional components configuration (assignments only )
799+ # Handle optional components configuration (streamlined mappings )
787800 components_dict = config_dict .get ("components" , {})
788801 if not isinstance (components_dict , dict ):
789802 raise ValueError ("'components' configuration section must be a dictionary" )
@@ -806,7 +819,21 @@ def _from_dict(cls, config_dict: dict[str, Any]) -> TopologyConfig:
806819 dc = dc_assignment ,
807820 )
808821
809- components = ComponentsConfig (assignments = assignments )
822+ # New streamlined mappings
823+ hw_component_map = components_dict .get ("hw_component" , {}) or {}
824+ optics_map = components_dict .get ("optics" , {}) or {}
825+ if hw_component_map is not None and not isinstance (hw_component_map , dict ):
826+ raise ValueError (
827+ "'components.hw_component' must be a mapping when provided"
828+ )
829+ if optics_map is not None and not isinstance (optics_map , dict ):
830+ raise ValueError ("'components.optics' must be a mapping when provided" )
831+
832+ components = ComponentsConfig (
833+ assignments = assignments ,
834+ hw_component = hw_component_map if isinstance (hw_component_map , dict ) else {},
835+ optics = optics_map if isinstance (optics_map , dict ) else {},
836+ )
810837
811838 # Handle optional failure_policies configuration (assignments only)
812839 failure_policies_dict = config_dict .get ("failure_policies" , {})
0 commit comments