Skip to content

feat(sonic): validate BGP_NEIGHBOR_AF key references - #2619

Open
ideaship wants to merge 2 commits into
mainfrom
sonic-validator-af-leafref
Open

feat(sonic): validate BGP_NEIGHBOR_AF key references#2619
ideaship wants to merge 2 commits into
mainfrom
sonic-validator-af-leafref

Conversation

@ideaship

@ideaship ideaship commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Problem

BGP_NEIGHBOR_AF.neighbor is a YANG leafref into BGP_NEIGHBOR, restricted to
the same VRF by an XPath predicate:

path "../../../BGP_NEIGHBOR/BGP_NEIGHBOR_LIST[vrf_name=current()/../vrf_name]/neighbor"

sonic-bgp-neighbor.yang#L138-L146

So the vrf_name|neighbor prefix of an AF row key must name an existing
neighbor. Nothing checks that. An AF row can activate an address family for a
peer that has no BGP_NEIGHBOR entry, while the neighbor that does exist is
left with no address family at all — a session that comes up and exchanges
nothing.

Why the generated table does not cover it

The validator now splits |-joined row keys using the key leaves the generator
records in TABLE_KEY_FIELDS, and it does validate part of this very key: the
vrf_name component is checked against BGP_GLOBALS. What it cannot check is
neighbor, because no constraint exists for it —
parse_leafref_path() returns None for relative paths (../…) and
for any path carrying an XPath predicate ([…]), and this leafref is both. So
_generated/_leafrefs.py emits nine constraints for BGP_NEIGHBOR_AF — the
route-maps, prefix-lists, AS-path sets and vrf_name — and none for the leaf
that decides whether the row refers to a real peer.

KEY_PREFIX_REFS is therefore hand-maintained, and lives beside the validator
logic rather than in _generated/, which is marked do-not-edit. Each entry
records the YANG path it encodes.

Expressing the rule as a key prefix match is not a shortcut — the leafref
plus its vrf_name predicate is exactly "the first two key components must
match", so VRF scoping falls out rather than being bolted on.

Second commit

A regression test only, and worth saying why it is not a fix.

_iter_leafref_values() evaluates "|" not in row_key for single-key tables.
A non-string row key makes that membership test raise TypeError, so
validate_config() propagates an exception instead of returning a
ValidationResult — the one thing a validator should not do, since the caller
cannot then distinguish a malformed config from a broken validator.

This branch originally carried a guard for it. main has since grown a broader
one: _iter_leafref_values() yields nothing for a non-string row key before
reaching the membership test, which covers every shape rather than that one
call. The guard is dropped here as redundant and the test kept, so the
behaviour stays pinned.

Unreachable for JSON input, whose object keys are always strings; reachable
through the in-memory dict the function also accepts.

Verification

Test-first; the tests failed in two different functions, which is what
surfaced the second bug.

Measured over every ConfigDB artifact available, with the check counting only
its own errors:

input AF rows flagged
E2E goldens as they stood before the keying fix 8 8
E2E goldens as they stand now, keying fixed 12 0
two config_db.json from a live fleet 74 0

Every instance of the broken shape is caught, and nothing in 86 rows of
coherent config is. The goldens appear twice on purpose: the same check reads
them as entirely broken before the keying fix and entirely clean after, which
is the distinction it exists to draw. None of those artifacts are in this
branch — the goldens live on a draft series, the fleet configs are not public
— so the figures are not reproducible from it alone.

3249 passed in tests/unit (3 pre-existing xfails), flake8 and black clean.

Scope

Nothing consumes this automatically during a sync: validate_config() is
reached through osism sonic validate and through the unit test that runs the
validator over committed ConfigDB artifacts. No artifact in main carries a
BGP_NEIGHBOR_AF row today, so this changes no pipeline result — it makes the
defect reportable, and gives the keying fix something to be measured against:

🤖 Generated with Claude Code

sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Aug 25, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ideaship
ideaship changed the base branch from sonic-bgp-af-admin-status to main August 30, 2026 15:02
@ideaship
ideaship force-pushed the sonic-validator-af-leafref branch from f9da38d to 134514d Compare August 30, 2026 15:06
BGP_NEIGHBOR_AF.neighbor is a YANG leafref into BGP_NEIGHBOR restricted
to the same VRF, so the vrf_name|neighbor prefix of an AF row key must
name an existing neighbor. Nothing checked that. An AF row could
activate an address family for a peer with no BGP_NEIGHBOR entry, while
the neighbor that does exist was left with no address family at all --
a session that comes up and exchanges nothing.

The generated constraint table cannot express it. The leafref path is
both relative and predicated:

    ../../../BGP_NEIGHBOR/BGP_NEIGHBOR_LIST[vrf_name=current()/../vrf_name]/neighbor

and parse_leafref_path() in tools/sonic_yang_to_pydantic.py returns None
for either shape, so no constraint is emitted for this leaf. Composite
row keys are otherwise covered: of the nine constraints the generator
does emit for this table, vrf_name is read out of the row key and
checked against BGP_GLOBALS. Only the component that decides whether the
row names a real peer goes unchecked.

KEY_PREFIX_REFS is therefore hand-maintained and lives beside the
validator logic rather than in _generated/, which is marked do-not-edit.

Rows are skipped when their key has too few components, or when the key
is not a string: both are malformed rows that the row schema already
reports, and reporting them here too would turn one defect into two.

Measured over the E2E goldens and two config_db.json from a live fleet,
none of which are in tree: all 8 address-family rows the goldens carried
before the keying fix are flagged, none of the 12 they carry after it
are, and none of the 74 fleet rows are. The check separates the two
shapes it exists to distinguish.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
_iter_leafref_values() evaluates "|" not in row_key for tables whose
list has a single key. A non-string row key makes that membership test
raise TypeError, so validate_config() propagates an exception instead of
returning a ValidationResult -- the one thing a validator should never
do, since the caller cannot tell a malformed config from a broken
validator.

This started as a guard on that membership test. main has since grown a
broader one: _iter_leafref_values() yields nothing for a non-string row
key before the test is reached, which covers every shape rather than
that one call. The guard is dropped here as redundant, leaving the test,
so the behaviour stays pinned either way.

JSON object keys are always strings, so the case is out of reach for a
config read from a file. It is reachable through the in-memory dict the
function also accepts, and through any loader producing non-string keys.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
@ideaship
ideaship force-pushed the sonic-validator-af-leafref branch from 134514d to c864541 Compare August 31, 2026 08:48
@sourcery-ai
sourcery-ai Bot dismissed their stale review August 31, 2026 08:49

Sourcery withdrew this approval because the latest commits introduced blocking findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

3 participants