diff --git a/osism/tasks/conductor/sonic/_generated/__init__.py b/osism/tasks/conductor/sonic/_generated/__init__.py index 7ac14c24d..a29b4b4bd 100644 --- a/osism/tasks/conductor/sonic/_generated/__init__.py +++ b/osism/tasks/conductor/sonic/_generated/__init__.py @@ -2,6 +2,7 @@ # AUTO-GENERATED — DO NOT EDIT BY HAND. """Generated SONiC ConfigDB schemas.""" +from ._leafrefs import LEAFREFS, LeafrefConstraint from ._schemas import TABLE_MODELS -__all__ = ["TABLE_MODELS"] +__all__ = ["LEAFREFS", "LeafrefConstraint", "TABLE_MODELS"] diff --git a/osism/tasks/conductor/sonic/_generated/_leafrefs.py b/osism/tasks/conductor/sonic/_generated/_leafrefs.py new file mode 100644 index 000000000..62c13d313 --- /dev/null +++ b/osism/tasks/conductor/sonic/_generated/_leafrefs.py @@ -0,0 +1,769 @@ +# SPDX-License-Identifier: Apache-2.0 +# AUTO-GENERATED — DO NOT EDIT BY HAND. +# Regenerate with: python tools/sonic_yang_to_pydantic.py +# flake8: noqa: E501 +"""SONiC ConfigDB cross-table leafref constraints.""" + +from dataclasses import dataclass +from typing import Tuple + + +@dataclass(frozen=True) +class LeafrefConstraint: + """A leafref from ``source_table.source_field`` to one of ``targets``.""" + + source_table: str + source_field: str + targets: Tuple[Tuple[str, str], ...] + is_leaf_list: bool = False + source_is_simple_key: bool = False + + +LEAFREFS: Tuple[LeafrefConstraint, ...] = ( + LeafrefConstraint( + source_table="BGP_GLOBALS", + source_field="vrf_name", + targets=(("VRF", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="BGP_GLOBALS_AF", + source_field="import_vrf_route_map", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_GLOBALS_AF", + source_field="route_download_filter", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_GLOBALS_AF_AGGREGATE_ADDR", + source_field="policy", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_GLOBALS_AF_NETWORK", + source_field="policy", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_GLOBALS_LISTEN_PREFIX", + source_field="vrf_name", + targets=(("BGP_GLOBALS", "vrf_name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR", + source_field="local_addr", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR", + source_field="neighbor", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR", + source_field="vrf_name", + targets=(("BGP_GLOBALS", "vrf_name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="default_rmap", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="filter_list_in", + targets=(("AS_PATH_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="filter_list_out", + targets=(("AS_PATH_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="prefix_list_in", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="prefix_list_out", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="route_map_in", + targets=(("ROUTE_MAP_SET", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="route_map_out", + targets=(("ROUTE_MAP_SET", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="unsuppress_map_name", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_NEIGHBOR_AF", + source_field="vrf_name", + targets=(("BGP_GLOBALS", "vrf_name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP", + source_field="local_addr", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP", + source_field="vrf_name", + targets=(("BGP_GLOBALS", "vrf_name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="default_rmap", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="filter_list_in", + targets=(("AS_PATH_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="filter_list_out", + targets=(("AS_PATH_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="prefix_list_in", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="prefix_list_out", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="route_map_in", + targets=(("ROUTE_MAP_SET", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="route_map_out", + targets=(("ROUTE_MAP_SET", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="unsuppress_map_name", + targets=(("ROUTE_MAP_SET", "name"),), + ), + LeafrefConstraint( + source_table="BGP_PEER_GROUP_AF", + source_field="vrf_name", + targets=(("BGP_GLOBALS", "vrf_name"),), + ), + LeafrefConstraint( + source_table="BUFFER_PG", + source_field="port", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="BUFFER_PG", + source_field="profile", + targets=(("BUFFER_PROFILE", "name"),), + ), + LeafrefConstraint( + source_table="BUFFER_PORT_EGRESS_PROFILE_LIST", + source_field="port", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="BUFFER_PORT_EGRESS_PROFILE_LIST", + source_field="profile_list", + targets=(("BUFFER_PROFILE", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BUFFER_PORT_INGRESS_PROFILE_LIST", + source_field="port", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="BUFFER_PORT_INGRESS_PROFILE_LIST", + source_field="profile_list", + targets=(("BUFFER_PROFILE", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="BUFFER_PROFILE", + source_field="pool", + targets=(("BUFFER_POOL", "name"),), + ), + LeafrefConstraint( + source_table="BUFFER_QUEUE", + source_field="port", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="BUFFER_QUEUE", + source_field="profile", + targets=(("BUFFER_PROFILE", "name"),), + ), + LeafrefConstraint( + source_table="COPP_TRAP", + source_field="trap_group", + targets=(("COPP_GROUP", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ACL_IN", + source_field="acl_group_id", + targets=(("DASH_ACL_GROUP", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ACL_IN", + source_field="eni", + targets=(("DASH_ENI", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ACL_OUT", + source_field="acl_group_id", + targets=(("DASH_ACL_GROUP", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ACL_OUT", + source_field="eni", + targets=(("DASH_ENI", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ACL_RULE", + source_field="acl_group_id", + targets=(("DASH_ACL_GROUP", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ENI", + source_field="qos", + targets=(("DASH_QOS", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ENI", + source_field="vnet", + targets=(("DASH_VNET", "name"),), + ), + LeafrefConstraint( + source_table="DASH_HA_GLOBAL_CONFIG", + source_field="vnet_name", + targets=(("VNET", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ROUTE_TABLE", + source_field="action_type", + targets=(("DASH_ROUTING_TYPE", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ROUTE_TABLE", + source_field="appliance", + targets=(("DASH_APPLIANCE", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ROUTE_TABLE", + source_field="eni", + targets=(("DASH_ENI", "name"),), + ), + LeafrefConstraint( + source_table="DASH_ROUTE_TABLE", + source_field="vnet", + targets=(("DASH_VNET", "name"),), + ), + LeafrefConstraint( + source_table="DASH_VNET_MAPPING_TABLE", + source_field="routing_type", + targets=(("DASH_ROUTING_TYPE", "name"),), + ), + LeafrefConstraint( + source_table="DASH_VNET_MAPPING_TABLE", + source_field="vnet", + targets=(("DASH_VNET", "name"),), + ), + LeafrefConstraint( + source_table="DEVICE_NEIGHBOR", + source_field="local_port", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="DHCPV4_RELAY", + source_field="server_vrf", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="DHCPV4_RELAY", + source_field="source_interface", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ), + ), + LeafrefConstraint( + source_table="DHCP_SERVER_IPV4", + source_field="customized_options", + targets=(("DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="DHCP_SERVER_IPV4", + source_field="name", + targets=(("MID_PLANE_BRIDGE", "bridge"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="DHCP_SERVER_IPV4_PORT", + source_field="name", + targets=(("DHCP_SERVER_IPV4", "name"),), + ), + LeafrefConstraint( + source_table="DHCP_SERVER_IPV4_PORT", + source_field="port", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("DPUS", "midplane_interface"), + ), + ), + LeafrefConstraint( + source_table="DHCP_SERVER_IPV4_PORT", + source_field="ranges", + targets=(("DHCP_SERVER_IPV4_RANGE", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="FG_NHG_MEMBER", + source_field="FG_NHG", + targets=(("FG_NHG", "name"),), + ), + LeafrefConstraint( + source_table="FG_NHG_MEMBER", + source_field="link", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + ), + LeafrefConstraint( + source_table="FG_NHG_PREFIX", + source_field="FG_NHG", + targets=(("FG_NHG", "name"),), + ), + LeafrefConstraint( + source_table="HIGH_FREQUENCY_TELEMETRY_GROUP", + source_field="profile_name", + targets=(("HIGH_FREQUENCY_TELEMETRY_PROFILE", "name"),), + ), + LeafrefConstraint( + source_table="INTERFACE", + source_field="name", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="INTERFACE", + source_field="vnet_name", + targets=(("VNET", "name"),), + ), + LeafrefConstraint( + source_table="INTERFACE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="LLDP_PORT", + source_field="ifname", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="LOOPBACK_INTERFACE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="MCLAG_DOMAIN", + source_field="peer_link", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + ), + LeafrefConstraint( + source_table="MCLAG_INTERFACE", + source_field="if_name", + targets=(("PORTCHANNEL", "name"),), + ), + LeafrefConstraint( + source_table="MGMT_INTERFACE", + source_field="name", + targets=(("MGMT_PORT", "name"),), + ), + LeafrefConstraint( + source_table="MIRROR_SESSION", + source_field="dst_port", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="MIRROR_SESSION", + source_field="policer", + targets=(("POLICER", "name"),), + ), + LeafrefConstraint( + source_table="MUX_CABLE", + source_field="ifname", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="NEIGH", + source_field="port", + targets=(("PORTCHANNEL", "name"), ("PORT", "name")), + ), + LeafrefConstraint( + source_table="NTP", + source_field="src_intf", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ("MGMT_PORT", "name"), + ), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="NTP_SERVER", + source_field="key", + targets=(("NTP_KEY", "id"),), + ), + LeafrefConstraint( + source_table="NVGRE_TUNNEL_MAP", + source_field="tunnel_name", + targets=(("NVGRE_TUNNEL", "tunnel_name"),), + ), + LeafrefConstraint( + source_table="PBH_HASH", + source_field="hash_field_list", + targets=(("PBH_HASH_FIELD", "hash_field_name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="PBH_RULE", + source_field="hash", + targets=(("PBH_HASH", "hash_name"),), + ), + LeafrefConstraint( + source_table="PBH_RULE", + source_field="table_name", + targets=(("PBH_TABLE", "table_name"),), + ), + LeafrefConstraint( + source_table="PBH_TABLE", + source_field="interface_list", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="PFC_WD", + source_field="ifname", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="PORT", + source_field="macsec", + targets=(("MACSEC_PROFILE", "name"),), + ), + LeafrefConstraint( + source_table="PORTCHANNEL_INTERFACE", + source_field="name", + targets=(("PORTCHANNEL", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="PORTCHANNEL_INTERFACE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="PORTCHANNEL_MEMBER", + source_field="name", + targets=(("PORTCHANNEL", "name"),), + ), + LeafrefConstraint( + source_table="PORTCHANNEL_MEMBER", + source_field="port", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="dot1p_to_tc_map", + targets=(("DOT1P_TO_TC_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="dscp_to_tc_map", + targets=(("DSCP_TO_TC_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="ifname", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="pfc_to_pg_map", + targets=(("PFC_PRIORITY_TO_PRIORITY_GROUP_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="pfc_to_queue_map", + targets=(("MAP_PFC_PRIORITY_TO_QUEUE", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="scheduler", + targets=(("SCHEDULER", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="tc_to_dscp_map", + targets=(("TC_TO_DSCP_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="tc_to_pg_map", + targets=(("TC_TO_PRIORITY_GROUP_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_QOS_MAP", + source_field="tc_to_queue_map", + targets=(("TC_TO_QUEUE_MAP", "name"),), + ), + LeafrefConstraint( + source_table="PORT_STORM_CONTROL", + source_field="ifname", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="QUEUE", + source_field="ifname", + targets=(("PORT", "name"),), + ), + LeafrefConstraint( + source_table="QUEUE", + source_field="scheduler", + targets=(("SCHEDULER", "name"),), + ), + LeafrefConstraint( + source_table="QUEUE", + source_field="wred_profile", + targets=(("WRED_PROFILE", "name"),), + ), + LeafrefConstraint( + source_table="RADIUS_SERVER", + source_field="src_intf", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ("MGMT_PORT", "name"), + ), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_as_path", + targets=(("AS_PATH_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_community", + targets=(("COMMUNITY_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_ext_community", + targets=(("EXTENDED_COMMUNITY_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_interface", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_ipv6_prefix_set", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_neighbor", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_next_hop_set", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_prefix_set", + targets=(("PREFIX_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="match_src_vrf", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="set_community_ref", + targets=(("COMMUNITY_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_MAP", + source_field="set_ext_community_ref", + targets=(("EXTENDED_COMMUNITY_SET", "name"),), + ), + LeafrefConstraint( + source_table="ROUTE_REDISTRIBUTE", + source_field="route_map", + targets=(("ROUTE_MAP_SET", "name"),), + is_leaf_list=True, + ), + LeafrefConstraint( + source_table="ROUTE_REDISTRIBUTE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="SFLOW", + source_field="agent_id", + targets=(("PORT", "name"), ("PORTCHANNEL", "name"), ("MGMT_PORT", "name")), + ), + LeafrefConstraint( + source_table="SFLOW_SESSION", + source_field="port", + targets=(("PORT", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="SRV6_MY_LOCATORS", + source_field="vrf", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="SRV6_MY_SIDS", + source_field="decap_vrf", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="SRV6_MY_SIDS", + source_field="locator", + targets=(("SRV6_MY_LOCATORS", "locator_name"),), + ), + LeafrefConstraint( + source_table="SYSLOG_CONFIG_FEATURE", + source_field="service", + targets=(("FEATURE", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="SYSLOG_SERVER", + source_field="vrf", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="TACPLUS", + source_field="src_intf", + targets=( + ("PORT", "name"), + ("PORTCHANNEL", "name"), + ("LOOPBACK_INTERFACE", "name"), + ("MGMT_PORT", "name"), + ), + ), + LeafrefConstraint( + source_table="TUNNEL", + source_field="src_ip", + targets=(("PEER_SWITCH", "address_ipv4"),), + ), + LeafrefConstraint( + source_table="VLAN_INTERFACE", + source_field="name", + targets=(("VLAN", "name"),), + source_is_simple_key=True, + ), + LeafrefConstraint( + source_table="VLAN_INTERFACE", + source_field="vnet_name", + targets=(("VNET", "name"),), + ), + LeafrefConstraint( + source_table="VLAN_INTERFACE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="VLAN_MEMBER", + source_field="name", + targets=(("VLAN", "name"),), + ), + LeafrefConstraint( + source_table="VLAN_MEMBER", + source_field="port", + targets=(("PORT", "name"), ("PORTCHANNEL", "name")), + ), + LeafrefConstraint( + source_table="VLAN_SUB_INTERFACE", + source_field="vnet_name", + targets=(("VNET", "name"),), + ), + LeafrefConstraint( + source_table="VLAN_SUB_INTERFACE", + source_field="vrf_name", + targets=(("VRF", "name"),), + ), + LeafrefConstraint( + source_table="VNET", + source_field="vxlan_tunnel", + targets=(("VXLAN_TUNNEL", "name"),), + ), + LeafrefConstraint( + source_table="VNET_ROUTE_TUNNEL", + source_field="vnet_name", + targets=(("VNET", "name"),), + ), + LeafrefConstraint( + source_table="VXLAN_EVPN_NVO", + source_field="source_vtep", + targets=(("VXLAN_TUNNEL", "name"),), + ), + LeafrefConstraint( + source_table="VXLAN_TUNNEL_MAP", + source_field="name", + targets=(("VXLAN_TUNNEL", "name"),), + ), +) diff --git a/osism/tasks/conductor/sonic/validator.py b/osism/tasks/conductor/sonic/validator.py index 375bb841b..1b96d10b5 100644 --- a/osism/tasks/conductor/sonic/validator.py +++ b/osism/tasks/conductor/sonic/validator.py @@ -9,11 +9,15 @@ """ from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Iterable, List, Optional from pydantic import ValidationError as PydValidationError -from osism.tasks.conductor.sonic._generated import TABLE_MODELS +from osism.tasks.conductor.sonic._generated import ( + LEAFREFS, + LeafrefConstraint, + TABLE_MODELS, +) @dataclass @@ -80,4 +84,103 @@ def validate_config(config: Dict[str, Any]) -> ValidationResult: ) ) + errors.extend(_check_leafrefs(config)) + return ValidationResult(valid=not errors, errors=errors, warnings=warnings) + + +def _check_leafrefs(config: Dict[str, Any]) -> List[ValidationError]: + """Verify every cross-table leafref reference resolves to an existing key. + + YANG `leafref` semantics say a leaf must point at an existing value in a + target path. In SONiC ConfigDB the row dict key carries the target list's + key value, so a missing reference is "value not in + ``config[target_table]``". Multi-target (union-of-leafref) succeeds if + *any* target accepts the value. + + Composite-key parsing is intentionally skipped — when the source field is + encoded only inside a `|`-separated row key, we can't safely split without + YANG key metadata, so we only check explicit row-dict fields plus the + ``source_is_simple_key`` shortcut where the row key alone is the value. + """ + errors: List[ValidationError] = [] + for constraint in LEAFREFS: + rows = config.get(constraint.source_table) + if not isinstance(rows, dict): + continue + target_keysets = _collect_target_keysets(config, constraint) + # If the config does not declare any of the target tables, the + # references are unresolvable — flag them. + for row_key, row in rows.items(): + for value in _iter_leafref_values(constraint, row_key, row): + if not _value_in_any_target(value, target_keysets): + errors.append( + ValidationError( + message=_format_missing_message(constraint, value), + path=f"{row_key}.{constraint.source_field}", + table=constraint.source_table, + ) + ) + return errors + + +def _collect_target_keysets( + config: Dict[str, Any], constraint: LeafrefConstraint +) -> List[set]: + """Return one set of legal values per target. ``target_field == "name"`` + (the list key) is the common case and corresponds to row keys; for + non-key targets we also accept matching values inside the rows.""" + keysets: List[set] = [] + for target_table, target_field in constraint.targets: + rows = config.get(target_table) + keys: set = set() + if isinstance(rows, dict): + for k, v in rows.items(): + keys.add(k) + if isinstance(v, dict): + inner = v.get(target_field) + if isinstance(inner, str): + keys.add(inner) + elif isinstance(inner, list): + for item in inner: + if isinstance(item, str): + keys.add(item) + keysets.append(keys) + return keysets + + +def _iter_leafref_values( + constraint: LeafrefConstraint, row_key: str, row: Any +) -> Iterable[str]: + """Yield the values from one row that this constraint should validate.""" + raw: Any = None + if isinstance(row, dict) and constraint.source_field in row: + raw = row[constraint.source_field] + elif constraint.source_is_simple_key and "|" not in row_key: + # Single-key list: row key directly carries the leaf value. + raw = row_key + + if raw is None: + return + if constraint.is_leaf_list: + if isinstance(raw, list): + for item in raw: + if isinstance(item, str): + yield item + elif isinstance(raw, str): + yield raw + else: + if isinstance(raw, str): + yield raw + + +def _value_in_any_target(value: str, keysets: List[set]) -> bool: + return any(value in ks for ks in keysets) + + +def _format_missing_message(constraint: LeafrefConstraint, value: str) -> str: + targets = ", ".join(f"{t}.{f}" for t, f in constraint.targets) + return ( + f"leafref {constraint.source_field}={value!r} does not resolve to " + f"an existing entry in {targets}" + ) diff --git a/tests/unit/tasks/conductor/sonic/test_validator.py b/tests/unit/tasks/conductor/sonic/test_validator.py new file mode 100644 index 000000000..3de928d34 --- /dev/null +++ b/tests/unit/tasks/conductor/sonic/test_validator.py @@ -0,0 +1,178 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the SONiC ConfigDB validator. + +Focus is on the cross-table leafref post-pass added in #2252; per-row schema +validation is exercised end-to-end by the existing CLI tests and only checked +here for non-regression. +""" + +from osism.tasks.conductor.sonic.validator import validate_config + + +def _leafref_errors(result): + return [e for e in result.errors if "leafref" in e.message] + + +def test_leafref_passes_when_target_exists(): + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + "PORTCHANNEL": {"PortChannel0": {"admin_status": "up"}}, + "PORTCHANNEL_MEMBER": { + "PortChannel0|Ethernet0": { + "name": "PortChannel0", + "port": "Ethernet0", + }, + }, + } + result = validate_config(config) + assert _leafref_errors(result) == [] + + +def test_leafref_fails_when_port_missing(): + """The headline AC: PORTCHANNEL_MEMBER → non-existent PORT must fail.""" + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + "PORTCHANNEL": {"PortChannel0": {"admin_status": "up"}}, + "PORTCHANNEL_MEMBER": { + "PortChannel0|Ethernet999": { + "name": "PortChannel0", + "port": "Ethernet999", + }, + }, + } + result = validate_config(config) + errors = _leafref_errors(result) + assert any( + e.table == "PORTCHANNEL_MEMBER" + and e.path == "PortChannel0|Ethernet999.port" + and "Ethernet999" in e.message + for e in errors + ), errors + + +def test_union_leafref_accepts_either_target(): + """VLAN_MEMBER.port is a union of PORT and PORTCHANNEL leafrefs.""" + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + "PORTCHANNEL": {"PortChannel0": {"admin_status": "up"}}, + "VLAN": {"Vlan100": {}}, + "VLAN_MEMBER": { + "Vlan100|Ethernet0": {"name": "Vlan100", "port": "Ethernet0"}, + "Vlan100|PortChannel0": {"name": "Vlan100", "port": "PortChannel0"}, + }, + } + result = validate_config(config) + assert _leafref_errors(result) == [] + + +def test_union_leafref_fails_when_no_target_matches(): + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + "PORTCHANNEL": {"PortChannel0": {"admin_status": "up"}}, + "VLAN": {"Vlan100": {}}, + "VLAN_MEMBER": { + "Vlan100|Ghost": {"name": "Vlan100", "port": "Ghost"}, + }, + } + result = validate_config(config) + errors = [e for e in _leafref_errors(result) if e.table == "VLAN_MEMBER"] + assert errors + msg = errors[0].message + assert "PORT.name" in msg and "PORTCHANNEL.name" in msg + + +def test_leafref_resolves_via_non_key_target_field(): + """TUNNEL.src_ip → PEER_SWITCH.address_ipv4: target_field is not the row key, + so the value must resolve via the inner field in `_collect_target_keysets`.""" + config = { + # Row key deliberately differs from address_ipv4 so the only way the + # leafref can resolve is via the inner non-key field. + "PEER_SWITCH": {"peer_switch_name": {"address_ipv4": "10.0.0.1"}}, + "TUNNEL": { + "MuxTunnel0": {"src_ip": "10.0.0.1", "tunnel_type": "IPINIP"}, + }, + } + result = validate_config(config) + assert [ + e for e in _leafref_errors(result) if e.table == "TUNNEL" + ] == [], result.errors + + +def test_leafref_fails_when_target_table_is_empty(): + """Empty target tables must not be treated as a wildcard match.""" + config = { + "BUFFER_PROFILE": {}, + "BUFFER_PORT_INGRESS_PROFILE_LIST": { + "Ethernet0": {"profile_list": ["p1"]}, + }, + # Seed PORT so the simple-key 'port' leafref on this table resolves + # and only the profile_list error remains. + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + } + result = validate_config(config) + errors = [ + e + for e in _leafref_errors(result) + if e.table == "BUFFER_PORT_INGRESS_PROFILE_LIST" + and "profile_list" in (e.path or "") + ] + assert any("p1" in e.message for e in errors), errors + + +def test_leaf_list_of_leafrefs_checks_each_element(): + config = { + "BUFFER_PROFILE": {"p1": {}, "p2": {}}, + "BUFFER_PORT_INGRESS_PROFILE_LIST": { + # row key carries the simple-key 'port' leafref → PORT.name; we + # don't want that to dominate this assertion, so seed PORT too. + "Ethernet0": {"profile_list": ["p1", "p2"]}, + "Ethernet1": {"profile_list": ["p1", "missing"]}, + }, + "PORT": { + "Ethernet0": {"lanes": "0", "speed": "10000"}, + "Ethernet1": {"lanes": "1", "speed": "10000"}, + }, + } + result = validate_config(config) + errors = [ + e + for e in _leafref_errors(result) + if e.table == "BUFFER_PORT_INGRESS_PROFILE_LIST" + and "profile_list" in (e.path or "") + ] + assert any("missing" in e.message for e in errors), errors + + +def test_simple_key_row_key_is_treated_as_leafref_value(): + """INTERFACE. is a leafref into PORT.name; the row dict often + has no explicit `name` field, so the validator must use the row key.""" + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + "INTERFACE": {"Ethernet999": {}}, + } + result = validate_config(config) + errors = [e for e in _leafref_errors(result) if e.table == "INTERFACE"] + assert any("Ethernet999" in e.message for e in errors), errors + + +def test_composite_row_key_skips_simple_key_shortcut(): + """When the row key contains '|' it's a composite key and we deliberately + don't try to split it; without an explicit field the leafref is skipped.""" + config = { + "PORT": {"Ethernet0": {"lanes": "0", "speed": "10000"}}, + # INTERFACE_IPPREFIX_LIST style key — composite. Not a real check today. + "INTERFACE": {"Ethernet0|10.0.0.1/31": {}}, + } + result = validate_config(config) + leafref_errors_for_interface = [ + e for e in _leafref_errors(result) if e.table == "INTERFACE" + ] + assert leafref_errors_for_interface == [] + + +def test_unknown_table_emits_warning_not_error(): + config = {"NOT_A_REAL_TABLE": {"x": {}}} + result = validate_config(config) + assert any("NOT_A_REAL_TABLE" in w for w in result.warnings) + assert _leafref_errors(result) == [] diff --git a/tools/sonic_yang_to_pydantic.py b/tools/sonic_yang_to_pydantic.py index da4336ded..564f6e529 100644 --- a/tools/sonic_yang_to_pydantic.py +++ b/tools/sonic_yang_to_pydantic.py @@ -24,7 +24,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple from pyang import context, repository # type: ignore[import-untyped] @@ -112,6 +112,117 @@ class PyType: annotation: str # e.g. "str", "Annotated[int, Field(ge=1, le=100)]" +@dataclass(frozen=True) +class LeafrefConstraint: + """One cross-table reference: ``source_table.source_field`` must point at + an existing key in *one of* ``targets`` (modeled as YANG `union of leafref`). + + ``is_leaf_list`` flags element-wise checks; ``source_is_simple_key`` is + true when the source leaf is the sole `key` of its YANG list, so the row + key in ConfigDB JSON directly carries the value. + """ + + source_table: str + source_field: str + targets: Tuple[Tuple[str, str], ...] + is_leaf_list: bool = False + source_is_simple_key: bool = False + + +def parse_leafref_path(path: str) -> Optional[Tuple[str, str]]: + """Parse a YANG `leafref` XPath and return ``(target_table, target_field)``. + + Handles the regular SONiC shape:: + + /:sonic-X/:TABLE/:TABLE_LIST/:field + + Returns ``None`` for relative paths (`../...`) and for any path containing + XPath predicates (`[...]`) — those need richer resolution and are tracked + as separate follow-ups. + """ + p = path.strip().strip('"').strip("'") + if not p.startswith("/"): + return None + if "[" in p or "]" in p: + return None + parts = [seg for seg in p.lstrip("/").split("/") if seg] + if len(parts) < 4: + return None + bare = [seg.split(":", 1)[-1] for seg in parts] + return bare[1], bare[-1] + + +def extract_leafref_targets(type_stmt) -> List[Tuple[str, str]]: + """Walk a YANG `type` statement and return every leafref target it + declares — directly, via `union`, or via a typedef. Order is preserved + and duplicates are removed.""" + base = type_stmt.arg + if base == "leafref": + path_stmt = type_stmt.search_one("path") + if path_stmt is None: + return [] + parsed = parse_leafref_path(path_stmt.arg) + return [parsed] if parsed else [] + if base == "union": + out: List[Tuple[str, str]] = [] + seen: set = set() + for s in type_stmt.substmts: + if s.keyword != "type": + continue + for tgt in extract_leafref_targets(s): + if tgt not in seen: + seen.add(tgt) + out.append(tgt) + return out + td = getattr(type_stmt, "i_typedef", None) + if td is not None: + inner = td.search_one("type") + if inner is not None: + return extract_leafref_targets(inner) + return [] + + +def list_keys(list_stmt) -> List[str]: + """Return the leaf names that form a YANG `list`'s key (empty if none).""" + key_stmt = list_stmt.search_one("key") + if key_stmt is None: + return [] + return key_stmt.arg.split() + + +def collect_leafref_constraints( + table_name: str, list_or_container, leaves +) -> List[LeafrefConstraint]: + """Inspect the leaves of one row schema and emit leafref constraints.""" + if list_or_container.keyword == "list": + keys = list_keys(list_or_container) + else: + keys = [] + constraints: List[LeafrefConstraint] = [] + for leaf in leaves: + type_stmt = leaf.search_one("type") + if type_stmt is None: + continue + targets = extract_leafref_targets(type_stmt) + if not targets: + continue + # Drop targets that point back at the same source (no-op self-refs). + targets = [t for t in targets if t != (table_name, leaf.arg)] + if not targets: + continue + is_simple_key = len(keys) == 1 and leaf.arg == keys[0] + constraints.append( + LeafrefConstraint( + source_table=table_name, + source_field=leaf.arg, + targets=tuple(targets), + is_leaf_list=(leaf.keyword == "leaf-list"), + source_is_simple_key=is_simple_key, + ) + ) + return constraints + + def parse_range_part(part: str) -> Tuple[Optional[int], Optional[int]]: part = part.strip() if ".." in part: @@ -345,9 +456,11 @@ def generate_row_class(class_name: str, leaves) -> str: ) -def generate_table(table_container) -> Tuple[str, str, str]: +def generate_table( + table_container, +) -> Tuple[str, str, str, List[LeafrefConstraint]]: """Generate code for one ConfigDB table container. - Returns (table_name, table_class, code_block). + Returns (table_name, table_class, code_block, leafref_constraints). Two patterns are recognised: 1. table → list+ → leafs (most common, e.g. PORT) @@ -361,17 +474,22 @@ def generate_table(table_container) -> Tuple[str, str, str]: parts: List[str] = [] row_classes: List[str] = [] + constraints: List[LeafrefConstraint] = [] if lists: for lst in lists: row_class = to_class_name(lst.arg) + "Row" - parts.append(generate_row_class(row_class, collect_leaves(lst))) + leaves = collect_leaves(lst) + parts.append(generate_row_class(row_class, leaves)) row_classes.append(row_class) + constraints.extend(collect_leafref_constraints(table_name, lst, leaves)) elif sub_containers: for sc in sub_containers: row_class = base + to_class_name(sc.arg) + "Row" - parts.append(generate_row_class(row_class, collect_leaves(sc))) + leaves = collect_leaves(sc) + parts.append(generate_row_class(row_class, leaves)) row_classes.append(row_class) + constraints.extend(collect_leafref_constraints(table_name, sc, leaves)) else: raise ValueError(f"table {table_name} has neither list nor container children") @@ -382,7 +500,7 @@ def generate_table(table_container) -> Tuple[str, str, str]: row_type = f"Union[{', '.join(row_classes)}]" parts.append(f"class {table_class}(RootModel[Dict[str, {row_type}]]):\n pass\n") - return table_name, table_class, "\n".join(parts) + return table_name, table_class, "\n".join(parts), constraints def load_yang_modules(yang_dir: Path): @@ -444,12 +562,13 @@ def main(argv: Optional[List[str]] = None) -> int: code_blocks: List[str] = [] registry: List[Tuple[str, str]] = [] + leafrefs: List[LeafrefConstraint] = [] skipped: List[Tuple[str, str]] = [] seen_tables: set = set() for path, module, container in find_table_containers(modules): try: - table_name, table_class, code = generate_table(container) + table_name, table_class, code, table_leafrefs = generate_table(container) except Exception as exc: # pragma: no cover - generator-time only skipped.append((container.arg, f"{path.name}: {exc}")) continue @@ -463,6 +582,7 @@ def main(argv: Optional[List[str]] = None) -> int: code_blocks.append(f"\n# {path.name} :: {module.arg} :: {table_name}\n{code}") registry.append((table_name, table_class)) + leafrefs.extend(table_leafrefs) body = "".join(code_blocks) body += "\n\nTABLE_MODELS: Dict[str, type[BaseModel]] = {\n" @@ -480,25 +600,108 @@ def main(argv: Optional[List[str]] = None) -> int: out_file = output / "_schemas.py" out_file.write_text(schema_code) + leafrefs_file = output / "_leafrefs.py" + leafrefs_file.write_text(render_leafrefs_module(leafrefs)) + init_file = output / "__init__.py" init_file.write_text( "# SPDX-License-Identifier: Apache-2.0\n" "# AUTO-GENERATED — DO NOT EDIT BY HAND.\n" '"""Generated SONiC ConfigDB schemas."""\n\n' + "from ._leafrefs import LEAFREFS, LeafrefConstraint\n" "from ._schemas import TABLE_MODELS\n\n" - '__all__ = ["TABLE_MODELS"]\n' + '__all__ = ["LEAFREFS", "LeafrefConstraint", "TABLE_MODELS"]\n' ) print(f"Wrote {len(registry)} table models -> {out_file}") + print(f"Wrote {len(leafrefs)} leafref constraints -> {leafrefs_file}") if skipped: print(f"Skipped {len(skipped)} containers:") for name, reason in skipped: print(f" - {name}: {reason}") - format_with_black(out_file, init_file) + format_with_black(out_file, leafrefs_file, init_file) return 0 +def render_leafrefs_module(constraints: List[LeafrefConstraint]) -> str: + """Render the auto-generated `_leafrefs.py` module. + + Constraints that share `(source_table, source_field)` — typically because + a table declares multiple `list` siblings with the same leafref leaf, e.g. + INTERFACE_LIST and INTERFACE_IPPREFIX_LIST both having `name` → + PORT/name — are merged: targets are unioned and the is_leaf_list / + source_is_simple_key flags become true if any contributing constraint had + them set. + """ + merged: Dict[Tuple[str, str], LeafrefConstraint] = {} + for c in constraints: + key = (c.source_table, c.source_field) + if key not in merged: + merged[key] = c + continue + existing = merged[key] + seen: set = set() + new_targets: List[Tuple[str, str]] = [] + for t in (*existing.targets, *c.targets): + if t not in seen: + seen.add(t) + new_targets.append(t) + merged[key] = LeafrefConstraint( + source_table=c.source_table, + source_field=c.source_field, + targets=tuple(new_targets), + is_leaf_list=existing.is_leaf_list or c.is_leaf_list, + source_is_simple_key=existing.source_is_simple_key + or c.source_is_simple_key, + ) + + sorted_constraints = sorted( + merged.values(), key=lambda c: (c.source_table, c.source_field) + ) + lines: List[str] = [] + lines.append("# SPDX-License-Identifier: Apache-2.0") + lines.append("# AUTO-GENERATED — DO NOT EDIT BY HAND.") + lines.append("# Regenerate with: python tools/sonic_yang_to_pydantic.py") + lines.append("# flake8: noqa: E501") + lines.append('"""SONiC ConfigDB cross-table leafref constraints."""') + lines.append("") + lines.append("from dataclasses import dataclass") + lines.append("from typing import Tuple") + lines.append("") + lines.append("") + lines.append("@dataclass(frozen=True)") + lines.append("class LeafrefConstraint:") + lines.append( + ' """A leafref from ``source_table.source_field`` to one of ``targets``."""' + ) + lines.append("") + lines.append(" source_table: str") + lines.append(" source_field: str") + lines.append(" targets: Tuple[Tuple[str, str], ...]") + lines.append(" is_leaf_list: bool = False") + lines.append(" source_is_simple_key: bool = False") + lines.append("") + lines.append("") + lines.append("LEAFREFS: Tuple[LeafrefConstraint, ...] = (") + for c in sorted_constraints: + targets_repr = ", ".join(f"({t[0]!r}, {t[1]!r})" for t in c.targets) + if len(c.targets) == 1: + targets_repr += "," + lines.append(" LeafrefConstraint(") + lines.append(f" source_table={c.source_table!r},") + lines.append(f" source_field={c.source_field!r},") + lines.append(f" targets=({targets_repr}),") + if c.is_leaf_list: + lines.append(" is_leaf_list=True,") + if c.source_is_simple_key: + lines.append(" source_is_simple_key=True,") + lines.append(" ),") + lines.append(")") + lines.append("") + return "\n".join(lines) + + def format_with_black(*paths: Path) -> None: """Run `black` on the generated files so the diff stays small and readable.""" cmd = [sys.executable, "-m", "black", "--quiet", *(str(p) for p in paths)]