Assemble a fabric from input objects, resolved per device like group_vars - #24
Open
mbakalarski wants to merge 5 commits into
Open
Assemble a fabric from input objects, resolved per device like group_vars#24mbakalarski wants to merge 5 commits into
mbakalarski wants to merge 5 commits into
Conversation
…vars A Fabric will name several input XRs, each carrying a fragment of the eos_designs document plus the devices that see it. This lands the resolution: per device, layer the inputs that apply, in order, with dict.update(). Nothing is merged. Two NodeSets carrying the same node-type key never meet, because no device sees both -- a dual-DC fabric's leaves each see their own l3leaf.defaults. That is what pyavd.get_avd_facts already expects to be handed; engine.render_fabric_design flattens it today by giving every device the same document, and xr.fabric_design_from_inputs exists to squeeze many hostvars back into one. Neither is needed on this path, so MergeOnSchema, a duplicate-key conflict rule and the defaults push-down are all avoided -- and with them any dependency on pyavd's private API. Two things that look like one are kept apart: which devices an input *declares* (the union of these is the fabric's device list, and there is no second list) and which devices *see* it (appliesTo). They coincide in simple topologies and diverge in a 5-stage CLOS, where a DC's super_spine block names four devices but is visible to all sixteen of that DC. Measured against AVD's own corpus rather than argued: the hostvars this produces are byte-identical to faithfully reproduced Ansible across all 8 bundled examples and every molecule scenario with an inventory of its own -- 25 inventories, up to 501 devices, in five seconds because nothing renders. The test is stricter than a render comparison on purpose, so a divergence cannot hide until it matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolution equivalence already proves the model: matching hostvars render identically, because it is the same function on the same input. So this adds nothing there, and that is not its job. It guards the pair (this path, this pyavd) -- an AVD upgrade that changes output slips past equivalence and fails here. That is what test_xr_fold does today through the fold, and this is its successor: the fold reaches 6 of the 8 examples, this reaches 7. campus-fabric is among the two the fold defers, with the reason "aaa_settings.radius differs by role; no node-scoped equivalent" -- there is no equivalent to find when nothing is folded, so it simply renders. Both nets run in parallel for now; removing the older one is a separate change, so the swap is visible in a diff rather than taken on trust. cv-pathfinder is deferred: its credentials are ansible-vault, and credentials cannot live in an XR spec. It carries XPASS semantics, so it will report itself the day that is fixed. Examples only -- the molecule scenarios need AVD features this path does not carry yet. Costs 5s: the offline suite goes 61 tests in 10.7s to 69 in 15.7s. Checked that the test can actually fail, by perturbing a golden value and watching it go red rather than by trusting that it would. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four XRDs -- NodeSet, NetworkServices, ConnectedEndpoints, Settings -- each carrying a fragment of the eos_designs document in an open spec.design, plus spec.appliesTo saying which devices see it. Fabric gains spec.requires, an ordered list naming its inputs. The kinds separate ownership, not content: RBAC is granted per kind, and eos_designs' top-level key names come from its own content, so no schema could partition them anyway. Only NodeSet declares devices. spec.declares is the fabric's device list and there is no second one -- a node named in a block the fabric does not declare is not a device. Never a pattern, either: visibility may be matched, existence may not, since a typo would silently drop devices. appliesTo takes all / nodeSets / hosts / matchHostnames. The pattern form copies AVD's own hostname matching from default_node_types -- and from the code, not the description: shared_utils/node_type.py anchors the pattern for you, while the schema's wording reads as though the author must. Copying the wording would have made the same pattern mean two things. A pattern matching nothing is an error rather than an empty set; kinds.unmatched_patterns() surfaces it, because a pattern is silent about matching nothing and the render is pushed as a full config replacement. Secret is in the requires enum from this first version. It is not implemented yet, but the mechanism it enables -- a Secret layered like any other input -- needs no other schema footprint, and adding the enum value after release would be a schema change to a published API. Credentials are not hypothetical here: the bundled examples carry sha512_password and type-7 BGP passwords, and type 7 is reversible, pyavd ships bgp_decrypt. All six XRDs gain categories [crossplane, netclab]. function-avd had them nowhere while netclab-xp carries them on all twelve, so `kubectl get netclab` returned nothing in the avd namespace. This lands the fix for Fabric and Device as well -- though it will only show on a fresh install, since Crossplane's dependency manager installs but does not upgrade. test_apis_consistency guards what a build cannot: that the XRDs and the kinds fn.py reconciles are the same set, that every XRD carries categories, and that each defaultCompositionRef resolves to a Composition for that kind. Checked it can fail, by dropping a categories block and watching it go red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Fabric asks Crossplane for each entry in spec.requires by kind, name and namespace, layers what comes back, and renders per device. A Fabric with no requires takes the released path unchanged: one document handed to every device. The gate is the point. Requirements are answered on the *next* reconcile, so the first one always arrives with nothing at all -- rendering then would push a fabric short of its inputs, as a full config replacement, with no Delete. So an unresolved requires composes nothing. The proto settles a question this design could not answer before: "not yet" and "never" are distinguishable. Crossplane sends an empty Resources for a requirement it looked for and did not find, and omits the key entirely when it has not fetched yet. Both gate, but they are different states and the condition says which -- WaitingForInputs against InputsMissing. Two refusals rather than a silent render, both because the alternative reaches a device. A matchHostnames pattern that matches nothing is fatal: a pattern is silent about matching nothing, so it cannot be allowed to be. A Secret named in requires is fatal too -- it is in the enum so the mechanism can land without a schema change, but rendering a fabric whose credentials are quietly absent is worse than not rendering. Values replaced by a later input are reported as a warning, never an error: the order is declared by whoever wrote requires, so an override is intentional. First tests in this repo to drive RunFunction. They cover both gate states, both refusals, that resolved inputs compose devices, that each input kind reconciles and reports its own keys, and that a fabric with only spec.design still composes -- the last one guarding the refactor that put both paths through render_structured_configs, since v0.1.6 is published and netclab-xp pins it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects in scripts/kind-up.sh, both found by running it rather than reading it, and both invisible to CI because CI does not run this script. The xpkg build passed no --examples-root. That is not "no examples": it means everything under examples/, including examples/lab/topology.yaml, which is helm values with no `kind`. So the build died with "Object 'Kind' is missing" -- the same failure that killed the v0.1.4 release. CI and the release workflow have named examples/fabric explicitly since, in four places, with comments citing that release. This script was the build path that never got the fix, so a local bring-up has been broken since the topology was committed in #19. And it installed two named XRDs while the package root ships whatever is under apis/, so a cluster built by this script no longer matched the package it was built from -- with the input kinds missing exactly where a Fabric that names them is being tested. It applies apis/*/ now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
Fabricgains four kinds of input object and an ordered list naming them.Each input carries a fragment of the eos_designs document plus the devices that
see it; per device, the inputs that apply are layered in order with
dict.update()— Ansible's defaulthash_behaviour=replace.Why nothing is merged
Two
NodeSets carrying the same node-type key never meet, because no devicesees both: in a dual-DC fabric each leaf sees its own
l3leaf.defaults. That iswhat
pyavd.get_avd_factsalready expects to be handed —engine.render_fabric_designflattens it today by giving every device the samedocument, and
xr.fabric_design_from_inputsexists to squeeze many hostvarsback into one. Neither is needed here, so a schema-aware merge, a duplicate-key
conflict rule and the
defaultspush-down are all avoided, and with them anydependency on pyavd's private API.
The API
NodeSet<role>.defaults,.nodes[],.node_groups[]NetworkServicesnetwork_services_keys.name)ConnectedEndpointsconnected_endpoints_keys.keylists,port_profiles,network_portsSettingsThe kinds separate ownership, not content: RBAC is granted per kind, and
eos_designs' top-level key names come from its own content, so no schema could
partition them anyway.
Only
NodeSetdeclares devices.spec.declaresis the fabric's device listand there is no second one — a node named in a block the fabric does not declare
is not a device. Never a pattern either: visibility may be matched, existence
may not, since a typo would silently drop devices.
appliesTotakesall/nodeSets/hosts/matchHostnames. Thepattern form copies AVD's own hostname matching from
default_node_types— andfrom the code, not the description:
shared_utils/node_type.pyanchors thepattern for you, while the schema's wording reads as though the author must.
Copying the wording would have made the same pattern mean two things. A pattern
matching nothing is an error rather than an empty set, because a pattern is
silent about matching nothing and the render is pushed as a full config
replacement.
Secretis in therequiresenum from this first version. Not implementedyet — but the mechanism it enables, a Secret layered like any other input, needs
no other schema footprint, and adding the enum value after release would be a
schema change to a published API. Credentials are not hypothetical: the bundled
examples carry
sha512_passwordand type-7 BGP passwords, and type 7 isreversible — pyavd ships
bgp_decrypt.All six XRDs gain
categories: [crossplane, netclab]. function-avd had themnowhere while netclab-xp carries them on all twelve, so
kubectl get netclabreturned nothing in the
avdnamespace. Fabric and Device are fixed here too,though it will only show on a fresh install — Crossplane's dependency manager
installs but does not upgrade.
Evidence
Measured against AVD's own corpus rather than argued.
cv-pathfinderdeferredavd-verify-kindsandavd-verify-kinds --renderrun both from the CLI.Three tests, doing different jobs
test_resolves_identically_to_ansibleis the guard on the model, and it isstricter than a render comparison on purpose: it fails on a hostvar difference
even where AVD would render the same config, so a divergence cannot hide until
it matters. Five seconds for the whole corpus, because nothing renders.
test_render_reproduces_goldenadds nothing there — matching hostvars renderidentically. It guards pyavd changing under us. That is what
test_xr_folddoes today through the fold, and this is its successor: the fold reaches 6 of 8
examples, this reaches 7,
campus-fabricbeing among the fold's deferrals.Both nets run in parallel deliberately; removing the older one is a separate
change, so the swap shows up in a diff rather than being taken on trust.
test_apis_consistencyguards what a build cannot and a cluster would find onerelease later: that the XRDs and the kinds
fn.pyreconciles are the same set,that every XRD carries
categories, and that eachdefaultCompositionRefresolves to a Composition for that kind.
Checked, not assumed
dropping a
categoriesblock, rather than by trusting they would.apis/, now carrying all sixXRDs and Compositions.
uv sync --lockedclean; no version bump in this PR.Not in scope
_reconcile_inputdoes not fillstatus.devices: an input does not know thefabric's device list, so
appliesToonly resolves where inputs are collected —the Fabric will fill it. Collection itself (
requirements.resources), the gatethat stops a render before its inputs arrive, and the
Secretimplementationare the next steps.
🤖 Generated with Claude Code