yang2sdk 2026.10.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yang2sdk-2026.10.16/.env.example +9 -0
- yang2sdk-2026.10.16/.env.lab-device.example +19 -0
- yang2sdk-2026.10.16/.env.notconf.example +5 -0
- yang2sdk-2026.10.16/.env.srl-lab.example +6 -0
- yang2sdk-2026.10.16/.github/pull_request_template.md +41 -0
- yang2sdk-2026.10.16/.github/workflows/ci-nightly.yaml +38 -0
- yang2sdk-2026.10.16/.github/workflows/ci-pr.yaml +80 -0
- yang2sdk-2026.10.16/.github/workflows/ci-srl.yaml +216 -0
- yang2sdk-2026.10.16/.github/workflows/release.yaml +94 -0
- yang2sdk-2026.10.16/.gitignore +28 -0
- yang2sdk-2026.10.16/.python-version +1 -0
- yang2sdk-2026.10.16/AGENTS.md +310 -0
- yang2sdk-2026.10.16/CHANGELOG.md +985 -0
- yang2sdk-2026.10.16/CONTRIBUTING.md +75 -0
- yang2sdk-2026.10.16/LICENSE +201 -0
- yang2sdk-2026.10.16/NOTICE +2 -0
- yang2sdk-2026.10.16/PKG-INFO +141 -0
- yang2sdk-2026.10.16/README.md +110 -0
- yang2sdk-2026.10.16/RFCs/6241.md +77 -0
- yang2sdk-2026.10.16/RFCs/6242.md +32 -0
- yang2sdk-2026.10.16/RFCs/7950.md +235 -0
- yang2sdk-2026.10.16/RFCs/7951.md +43 -0
- yang2sdk-2026.10.16/RFCs/8040.md +47 -0
- yang2sdk-2026.10.16/RFCs/8341.md +37 -0
- yang2sdk-2026.10.16/RFCs/8342.md +192 -0
- yang2sdk-2026.10.16/RFCs/8526.md +136 -0
- yang2sdk-2026.10.16/RFCs/8527.md +28 -0
- yang2sdk-2026.10.16/SECURITY.md +68 -0
- yang2sdk-2026.10.16/docs/README.md +14 -0
- yang2sdk-2026.10.16/docs/dev.md +160 -0
- yang2sdk-2026.10.16/docs/lab.md +93 -0
- yang2sdk-2026.10.16/docs/usage.md +137 -0
- yang2sdk-2026.10.16/pyproject.toml +109 -0
- yang2sdk-2026.10.16/src/yang2sdk/__init__.py +13 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/__init__.py +1 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/canonicalize_ast.py +228 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/closure.py +202 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/compiler.py +601 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/downloader.py +269 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/features.py +116 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/model_gaps.py +664 -0
- yang2sdk-2026.10.16/src/yang2sdk/cli/sdk_verify.py +1652 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/__init__.py +1 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/plugin_wrapper.py +9 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/__init__.py +6 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/core.py +531 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/ir.py +1481 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/__init__.py.jinja +1 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/__init__.py.jinja +32 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/_base.py.jinja +297 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/models.py.jinja +48 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/__init__.py.jinja +42 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/_base.py.jinja +243 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/navigators.py.jinja +247 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/session_manager.py.jinja +672 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/__init__.py.jinja +1 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/__init__.py.jinja +35 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/_base.py.jinja +142 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/models.py.jinja +105 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/__init__.py.jinja +66 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/_base.py.jinja +520 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/navigators.py.jinja +190 -0
- yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/session_manager.py.jinja +252 -0
- yang2sdk-2026.10.16/src/yang2sdk/py.typed +0 -0
- yang2sdk-2026.10.16/tests/__init__.py +0 -0
- yang2sdk-2026.10.16/tests/conftest.py +548 -0
- yang2sdk-2026.10.16/tests/fixtures/golden/.gitkeep +0 -0
- yang2sdk-2026.10.16/tests/notconf/__init__.py +0 -0
- yang2sdk-2026.10.16/tests/notconf/compose.yaml +24 -0
- yang2sdk-2026.10.16/tests/notconf/matrix.json +117 -0
- yang2sdk-2026.10.16/tests/notconf/wait_healthy.py +82 -0
- yang2sdk-2026.10.16/tests/srl/.env.srl.example +8 -0
- yang2sdk-2026.10.16/tests/srl/PINNED.md +59 -0
- yang2sdk-2026.10.16/tests/srl/README.md +57 -0
- yang2sdk-2026.10.16/tests/srl/__init__.py +0 -0
- yang2sdk-2026.10.16/tests/srl/lab.py +112 -0
- yang2sdk-2026.10.16/tests/srl/srl-startup.cli +11 -0
- yang2sdk-2026.10.16/tests/srl/srl.clab.yml +14 -0
- yang2sdk-2026.10.16/tests/test_lab_device_netconf.py +283 -0
- yang2sdk-2026.10.16/tests/test_matrix.py +3995 -0
- yang2sdk-2026.10.16/tests/test_netconf_logging.py +358 -0
- yang2sdk-2026.10.16/tests/test_notconf_protocol.py +174 -0
- yang2sdk-2026.10.16/tests/test_sdk_generate.py +717 -0
- yang2sdk-2026.10.16/tests/test_sdk_verify_ci.py +97 -0
- yang2sdk-2026.10.16/tests/test_sdk_verify_debug.py +95 -0
- yang2sdk-2026.10.16/tests/test_srl_netconf.py +539 -0
- yang2sdk-2026.10.16/uv.lock +910 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
DEVICE_NAME = 'device_name'
|
|
2
|
+
# OS version: names the generated package (<device>_<os-version>) and feeds
|
|
3
|
+
# --device-version. Also read from $DEVICE_VERSION by the compilers.
|
|
4
|
+
DEVICE_VERSION = '1.4.0'
|
|
5
|
+
DEVICE_IP = '192.168.137.42'
|
|
6
|
+
DEVICE_USER = 'username'
|
|
7
|
+
DEVICE_PASS = 'password'
|
|
8
|
+
RESTCONF_PORT = 443
|
|
9
|
+
NETCONF_PORT = 830
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Real lab device (LAB ONLY, shared equipment). Names only, dummy values.
|
|
2
|
+
# The generated SDK and the test-suite resolve these names; nothing here is
|
|
3
|
+
# ever printed, logged, or committed. `.env` itself is gitignored.
|
|
4
|
+
#
|
|
5
|
+
# Fallback: the legacy DEVICE_* names keep working for `yang-downloader`
|
|
6
|
+
# and `sdk-verify`, so an existing .env does not need to change.
|
|
7
|
+
LAB_DEVICE_IP=192.0.2.10
|
|
8
|
+
LAB_DEVICE_NETCONF_PORT=830
|
|
9
|
+
LAB_DEVICE_USER=netconf-operator
|
|
10
|
+
LAB_DEVICE_PASS=change-me
|
|
11
|
+
|
|
12
|
+
# Optional: pin the device host key so the secure default (verify=True)
|
|
13
|
+
# works end to end. Without it the client fails closed, which is correct.
|
|
14
|
+
#LAB_DEVICE_KNOWN_HOSTS=/path/to/known_hosts
|
|
15
|
+
# Explicit lab opt-out; logs a warning and skips host-key verification.
|
|
16
|
+
#LAB_DEVICE_ALLOW_UNVERIFIED=1
|
|
17
|
+
|
|
18
|
+
# Gates the write test. Writes are off unless this is exactly 1.
|
|
19
|
+
LAB_DEVICE_ALLOW_WRITE=0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## What
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. What changes, and why now? -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- The problem this solves. If it fixes a bug, link the issue. -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] The five gate commands pass (ruff, ruff format, ty, pyrefly, offline tests)
|
|
12
|
+
- [ ] `AGENTS.md` still describes the behaviour accurately, if the change touches
|
|
13
|
+
the generator contract, the parity table, or a safety default
|
|
14
|
+
- [ ] New IR fields have template rendering **and** a generated sample that imports
|
|
15
|
+
- [ ] Protocol changes cite an RFC section (`RFCs/*.md`)
|
|
16
|
+
- [ ] No credential, device IP, or `temp/` output committed
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
Per `AGENTS.md`'s evidence rule — this is the part reviewers rely on most.
|
|
21
|
+
|
|
22
|
+
**Files changed:**
|
|
23
|
+
|
|
24
|
+
**Commands run:**
|
|
25
|
+
|
|
26
|
+
<!-- e.g. the five gate commands; `uv run yang2netconf temp/yang_modules/srl/*.yang --device srl` -->
|
|
27
|
+
|
|
28
|
+
**Verified:**
|
|
29
|
+
|
|
30
|
+
**Not verified / WIP:**
|
|
31
|
+
|
|
32
|
+
<!-- Be explicit. If you did not run the simulator matrix or a lab job, say so.
|
|
33
|
+
A "skip" is not a pass, and unverified work presented as verified is the
|
|
34
|
+
one failure mode this repo cannot recover from. -->
|
|
35
|
+
|
|
36
|
+
## Safety
|
|
37
|
+
|
|
38
|
+
- [ ] No secure default weakened (`verify=True`, `auto_commit=False`,
|
|
39
|
+
`replace()` completeness guard, no embedded credentials)
|
|
40
|
+
- [ ] Any device quirk is documented and isolated to a downstream adapter, not
|
|
41
|
+
baked into the generic path
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: ci-nightly
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: "0 2 * * *"
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
full-matrix:
|
|
9
|
+
runs-on: ubuntu-24.04
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
image:
|
|
14
|
+
- { package: notconf, tag: latest }
|
|
15
|
+
- { package: notconf-ietf, tag: latest }
|
|
16
|
+
- { package: notconf-sros, tag: "21.10" }
|
|
17
|
+
- { package: notconf-sros, tag: "22.2" }
|
|
18
|
+
- { package: notconf-junos, tag: 21.1R1 }
|
|
19
|
+
- { package: notconf-junos, tag: 23.4R1 }
|
|
20
|
+
- { package: notconf-cisco-xr, tag: "762" }
|
|
21
|
+
- { package: notconf-cisco-xr, tag: "771" }
|
|
22
|
+
- { package: notconf-cisco-xr, tag: "2411" }
|
|
23
|
+
- { package: notconf-cisco-xr, tag: "2531" }
|
|
24
|
+
- { package: notconf-cisco-nx, tag: 10.4-4 }
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v5
|
|
28
|
+
- run: uv sync --locked --group dev --extra lab
|
|
29
|
+
- run: >-
|
|
30
|
+
uv run pytest tests/test_notconf_protocol.py tests/test_sdk_generate.py
|
|
31
|
+
tests/test_sdk_verify_ci.py -v
|
|
32
|
+
env:
|
|
33
|
+
NOTCONF_RUN_INTEGRATION: "1"
|
|
34
|
+
NOTCONF_IMAGE_PACKAGE: ${{ matrix.image.package }}
|
|
35
|
+
NOTCONF_IMAGE_TAG: ${{ matrix.image.tag }}
|
|
36
|
+
- name: Collect container logs
|
|
37
|
+
if: always()
|
|
38
|
+
run: docker ps -a --format '{{.Names}}' | grep -e yang2sdk- || true
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: ci-pr
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint-typecheck:
|
|
9
|
+
runs-on: ubuntu-24.04
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- run: uv sync --locked --group dev --extra lab
|
|
14
|
+
# `uv run`, not `uvx`: the four gates are pinned exactly in the dev group
|
|
15
|
+
# so CI installs what uv.lock says. `uvx` would resolve the newest release
|
|
16
|
+
# on every run and could redden main with no commit to explain it.
|
|
17
|
+
- run: uv run ruff check .
|
|
18
|
+
- run: uv run ruff format --check .
|
|
19
|
+
- run: uv run ty check
|
|
20
|
+
- run: uv run pyrefly check
|
|
21
|
+
|
|
22
|
+
offline-tests:
|
|
23
|
+
needs: lint-typecheck
|
|
24
|
+
runs-on: ubuntu-24.04
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v5
|
|
28
|
+
- run: uv sync --locked --group dev --extra lab
|
|
29
|
+
- run: uv run pytest tests/test_matrix.py -v
|
|
30
|
+
|
|
31
|
+
smoke:
|
|
32
|
+
needs: lint-typecheck
|
|
33
|
+
runs-on: ubuntu-24.04
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
image:
|
|
38
|
+
- { package: notconf, tag: latest }
|
|
39
|
+
- { package: notconf-ietf, tag: latest }
|
|
40
|
+
- { package: notconf-sros, tag: "22.2" }
|
|
41
|
+
- { package: notconf-junos, tag: 23.4R1 }
|
|
42
|
+
- { package: notconf-cisco-xr, tag: "2411" }
|
|
43
|
+
- { package: notconf-cisco-nx, tag: 10.4-4 }
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: astral-sh/setup-uv@v5
|
|
47
|
+
- run: uv sync --locked --group dev --extra lab
|
|
48
|
+
# test_sdk_verify_ci.py drives `sdk-verify` (every reachable node + every RPC
|
|
49
|
+
# Input serialised) against the same container the other two reuse, so it
|
|
50
|
+
# adds no container startup. It was a manual lab harness in no workflow,
|
|
51
|
+
# which is how three shipped defects survived: the RESTCONF
|
|
52
|
+
# ancestor-envelope unwrap, the NETCONF augment-closure gap, and the
|
|
53
|
+
# ietf-notification-capabilities model hole. Blocking, per image.
|
|
54
|
+
- run: >-
|
|
55
|
+
uv run pytest tests/test_notconf_protocol.py tests/test_sdk_generate.py
|
|
56
|
+
tests/test_sdk_verify_ci.py -v
|
|
57
|
+
env:
|
|
58
|
+
NOTCONF_RUN_INTEGRATION: "1"
|
|
59
|
+
NOTCONF_IMAGE_PACKAGE: ${{ matrix.image.package }}
|
|
60
|
+
NOTCONF_IMAGE_TAG: ${{ matrix.image.tag }}
|
|
61
|
+
|
|
62
|
+
# ci-gate: the single required check on main. It `needs` every other job in
|
|
63
|
+
# this workflow, so the branch ruleset only has to require this one stable
|
|
64
|
+
# name instead of one context per job/matrix shard. When you add a job to
|
|
65
|
+
# this workflow, add it to ci-gate's `needs` in the same PR (forgetting it
|
|
66
|
+
# fails open: the new job simply isn't gated).
|
|
67
|
+
ci-gate:
|
|
68
|
+
needs: [lint-typecheck, offline-tests, smoke]
|
|
69
|
+
if: always()
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- name: Fail unless every ci-pr job succeeded
|
|
73
|
+
run: |
|
|
74
|
+
needs='${{ toJSON(needs) }}'
|
|
75
|
+
echo "$needs" | jq -c 'with_entries(.value = .value.result)'
|
|
76
|
+
if ! echo "$needs" | jq -e 'all(.[]; .result == "success")' > /dev/null; then
|
|
77
|
+
echo "::error::ci-gate: one or more ci-pr jobs did not succeed"
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
echo "all ci-pr jobs succeeded"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
name: ci-srl
|
|
2
|
+
# SR Linux (Nokia) in containerlab — the repo's only real *commercial* NOS.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists: every notconf image is a simulator built from a handful of
|
|
5
|
+
# IETF/vendor models, and the Groove G30 is a single lab device that is
|
|
6
|
+
# writable-running only. None of them exercise a full vendor model tree
|
|
7
|
+
# (hundreds of modules, deep augment closure, identityref leaves) through the
|
|
8
|
+
# generated client. SR Linux does, in a container, for free.
|
|
9
|
+
#
|
|
10
|
+
# NOT an NMDA device, contrary to an earlier revision of this file. Measured
|
|
11
|
+
# against SR Linux 25.10.1 from a raw <hello> (370 capabilities): its complete
|
|
12
|
+
# base-capability set is candidate, confirmed-commit, rollback-on-error,
|
|
13
|
+
# startup, url, validate, with-defaults, with-operational-defaults and
|
|
14
|
+
# yang-library. It ships the ietf-netconf-nmda *module* but does NOT advertise
|
|
15
|
+
# `:nmda:1.0` — precisely the false positive guarded in
|
|
16
|
+
# tests/test_matrix.py::test_nmda_is_detected_from_the_capability_uri_not_a_module_name.
|
|
17
|
+
# So this job must not be described as NMDA coverage.
|
|
18
|
+
#
|
|
19
|
+
# Cost: this repository is public, so standard GitHub-hosted runners are free.
|
|
20
|
+
# Do NOT move this job to a `*-large` runner — larger runners are billed even
|
|
21
|
+
# for public repositories.
|
|
22
|
+
#
|
|
23
|
+
# Severity is deliberately split:
|
|
24
|
+
# deploy / readiness / get-schema / generate -> BLOCKING. Zero coverage must
|
|
25
|
+
# never render as a green run, and each of these failing means the steps
|
|
26
|
+
# after it prove nothing.
|
|
27
|
+
# pytest / sdk-verify -> non-blocking
|
|
28
|
+
# (continue-on-error). Device boot timing and init races are real; a
|
|
29
|
+
# failure here is a signal to read, not a merge blocker.
|
|
30
|
+
# Every blocking step must also pass for pytest to run at all (the default
|
|
31
|
+
# `if: success()`), so a broken lab is never reported as "tests passed".
|
|
32
|
+
on:
|
|
33
|
+
pull_request:
|
|
34
|
+
workflow_dispatch:
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: ci-srl-${{ github.ref }}
|
|
38
|
+
cancel-in-progress: true
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
srl-containerlab:
|
|
42
|
+
# Standard runner, not a larger one: 4 vCPU / 16 GB RAM is above SR Linux's
|
|
43
|
+
# documented 2 vCPU / 4 GB minimum, and it keeps the job free for a public repo.
|
|
44
|
+
runs-on: ubuntu-24.04
|
|
45
|
+
timeout-minutes: 45
|
|
46
|
+
# Deliberately no `needs:`. GitHub Actions cannot express dependencies
|
|
47
|
+
# across workflow files, so this cannot wait on ci-pr.yaml's lint-typecheck
|
|
48
|
+
# job — it runs in parallel with ci-pr instead, and does its own `uv sync`.
|
|
49
|
+
# Since the pytest step is non-blocking, ordering against the real gates
|
|
50
|
+
# would buy nothing anyway.
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
|
|
55
|
+
- uses: astral-sh/setup-uv@v5
|
|
56
|
+
|
|
57
|
+
- name: Install containerlab (pinned)
|
|
58
|
+
# CLAB_VERSION is pinned for the same reason tests/srl/srl.clab.yml pins
|
|
59
|
+
# the SR Linux image: an unpinned containerlab release can change deploy
|
|
60
|
+
# or readiness behaviour overnight and turn a green pipeline red with no
|
|
61
|
+
# code change. Bump it deliberately, and note the bump in the PR.
|
|
62
|
+
# `install-containerlab` only — the runner already ships docker and has
|
|
63
|
+
# passwordless sudo, and "all" would try to install its own docker.
|
|
64
|
+
env:
|
|
65
|
+
CLAB_VERSION: "0.79.0"
|
|
66
|
+
run: |
|
|
67
|
+
curl -sL https://containerlab.dev/setup | sudo -E bash -s install-containerlab
|
|
68
|
+
containerlab version
|
|
69
|
+
|
|
70
|
+
- name: Install project
|
|
71
|
+
run: uv sync --locked --group dev --extra lab
|
|
72
|
+
|
|
73
|
+
- name: Deploy SR Linux lab
|
|
74
|
+
# `sudo -E` keeps $HOME so containerlab writes its lab state next to
|
|
75
|
+
# the topology file (tests/srl/clab-*/, already gitignored) instead of
|
|
76
|
+
# under /root. containerlab needs root for the privileged SR Linux node.
|
|
77
|
+
#
|
|
78
|
+
# Most likely failure mode on a new runner: SR Linux's DPDK-based XDP
|
|
79
|
+
# datapath needs the SSSE3 instruction set and containerlab *aborts*
|
|
80
|
+
# deploy without it. GitHub's AMD EPYC runners have SSSE3, but keep the
|
|
81
|
+
# full deploy output in the log so that diagnosis is one scroll away.
|
|
82
|
+
run: |
|
|
83
|
+
sudo -E containerlab deploy -t tests/srl/srl.clab.yml
|
|
84
|
+
|
|
85
|
+
- name: Wait for NETCONF readiness
|
|
86
|
+
# The lab is not usable the instant `deploy` returns: SR Linux takes
|
|
87
|
+
# minutes to bring up its management plane. tests/srl/lab.py polls the
|
|
88
|
+
# RFC 6241 Sec 8 hello with the lab's own credentials and exits
|
|
89
|
+
# non-zero on timeout, so a node that never boots fails here loudly
|
|
90
|
+
# rather than skipping every test below.
|
|
91
|
+
run: uv run python -m tests.srl.lab
|
|
92
|
+
|
|
93
|
+
- name: Download YANG from the node (get-schema)
|
|
94
|
+
# Same path the notconf suite uses: the shipped yang-downloader, so the
|
|
95
|
+
# integration suite exercises the code users actually run. It also
|
|
96
|
+
# writes features.json from the <hello> capability set, which
|
|
97
|
+
# yang2netconf picks up automatically as --features-file.
|
|
98
|
+
#
|
|
99
|
+
# Single source of truth for host/port/credentials: the committed
|
|
100
|
+
# tests/srl/.env.srl.example (published containerlab defaults, not
|
|
101
|
+
# secrets). It must be sourced *here* — every `run:` is a fresh shell,
|
|
102
|
+
# so exporting it in an earlier step would not survive. downloader.main()
|
|
103
|
+
# reads DEVICE_IP / NETCONF_PORT / DEVICE_USER / DEVICE_PASS /
|
|
104
|
+
# DEVICE_NAME from the environment and exits 2 if any is missing.
|
|
105
|
+
# DEVICE_NAME is what makes the output land in temp/yang_modules/srl.
|
|
106
|
+
#
|
|
107
|
+
# NOTE: downloader.main() discards download_all()'s failure count and
|
|
108
|
+
# still exits 0, so a partial extraction looks like success. Guard on
|
|
109
|
+
# the artefacts instead of trusting the exit code.
|
|
110
|
+
run: |
|
|
111
|
+
set -a
|
|
112
|
+
# shellcheck disable=SC1091
|
|
113
|
+
source tests/srl/.env.srl.example
|
|
114
|
+
set +a
|
|
115
|
+
echo "lab endpoint ${DEVICE_IP}:${NETCONF_PORT} as device '${DEVICE_NAME}'"
|
|
116
|
+
uv run yang-downloader
|
|
117
|
+
count=$(find temp/yang_modules/srl -name '*.yang' | wc -l)
|
|
118
|
+
echo "downloaded ${count} YANG modules"
|
|
119
|
+
if [ "$count" -lt 10 ]; then
|
|
120
|
+
echo "::error::get-schema returned only ${count} modules — refusing to compile"
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
test -f temp/yang_modules/srl/features.json \
|
|
124
|
+
|| { echo "::error::features.json missing — <hello> capability parse failed"; exit 1; }
|
|
125
|
+
|
|
126
|
+
- name: Generate the NETCONF client from the node's own models
|
|
127
|
+
# Roots are the two the tests navigate to: <system> (system/name/host-name,
|
|
128
|
+
# management, logging) and the interfaces list.
|
|
129
|
+
#
|
|
130
|
+
# yang2netconf now resolves the augment closure itself (88c2681), so the
|
|
131
|
+
# explicit closure.py step that used to live here is redundant — passing
|
|
132
|
+
# the two root files is enough. Kept as a single command so this job
|
|
133
|
+
# exercises the documented user path rather than a special one.
|
|
134
|
+
run: |
|
|
135
|
+
uv run yang2netconf \
|
|
136
|
+
temp/yang_modules/srl/srl_nokia-system@*.yang \
|
|
137
|
+
temp/yang_modules/srl/srl_nokia-interfaces@*.yang \
|
|
138
|
+
--device srl \
|
|
139
|
+
--yang-dir temp/yang_modules/srl \
|
|
140
|
+
--output-dir temp/netconf_clients/srl
|
|
141
|
+
test -f temp/netconf_clients/srl/__init__.py
|
|
142
|
+
|
|
143
|
+
- name: SR Linux NETCONF suite
|
|
144
|
+
# NON-BLOCKING on purpose. Everything above must pass for this step to
|
|
145
|
+
# even run (default `if: success()`), so a broken lab is still a red X.
|
|
146
|
+
# A failure *here* is a device/timing or generator-regression signal that
|
|
147
|
+
# should be read, not a merge blocker.
|
|
148
|
+
#
|
|
149
|
+
# Expect one skip: test_hostname_mutate_restore. A whole-<system> merge
|
|
150
|
+
# is not expressible against this device for two verified reasons —
|
|
151
|
+
# SR Linux rejects identityref writes in the RFC 7951 string form it
|
|
152
|
+
# itself emits, and a write below the root omits the ancestor path the
|
|
153
|
+
# device requires. Both are documented at tests/test_srl_netconf.py.
|
|
154
|
+
# This job's first run earned that skip: it is not a pre-existing
|
|
155
|
+
# condition.
|
|
156
|
+
id: srl-tests
|
|
157
|
+
continue-on-error: true
|
|
158
|
+
env:
|
|
159
|
+
NOTCONF_RUN_INTEGRATION: "1"
|
|
160
|
+
run: uv run pytest tests/test_srl_netconf.py -v
|
|
161
|
+
|
|
162
|
+
- name: sdk-verify against the full vendor tree
|
|
163
|
+
# NON-BLOCKING, same severity as the pytest step above. This is the
|
|
164
|
+
# exhaustive walk — every reachable node at bounded depth, every RPC
|
|
165
|
+
# Input serialised — over a tree the notconf simulators cannot match
|
|
166
|
+
# (397 modules, deep augment closure, identityref leaves).
|
|
167
|
+
#
|
|
168
|
+
# `crud` is enabled here and only here. It is a writer: it holds the
|
|
169
|
+
# candidate lock, merges, reads back, discards, and proves restoration
|
|
170
|
+
# with a whole-tree digest, and it refuses to run without BOTH
|
|
171
|
+
# --write and a second acknowledgement (--allow-running-writes here,
|
|
172
|
+
# since SR Linux has :candidate). That is why it does not belong in the
|
|
173
|
+
# notconf smoke matrix.
|
|
174
|
+
#
|
|
175
|
+
# Measured locally against 25.10.1: 291 pass / 0 fail / 170 skip, with
|
|
176
|
+
# `verify-restore: config digest matches snapshot`.
|
|
177
|
+
id: srl-sdk-verify
|
|
178
|
+
continue-on-error: true
|
|
179
|
+
run: |
|
|
180
|
+
set -a
|
|
181
|
+
# shellcheck disable=SC1091
|
|
182
|
+
source tests/srl/.env.srl.example
|
|
183
|
+
set +a
|
|
184
|
+
uv run sdk-verify \
|
|
185
|
+
--device srl \
|
|
186
|
+
--protocol netconf \
|
|
187
|
+
--tiers read,rpc,crud \
|
|
188
|
+
--write \
|
|
189
|
+
--allow-running-writes \
|
|
190
|
+
--no-verify-tls \
|
|
191
|
+
--snapshot temp/verify/srl-netconf-crud-snapshot.json \
|
|
192
|
+
--json-out temp/verify/srl-netconf-report.json
|
|
193
|
+
|
|
194
|
+
- name: Container logs
|
|
195
|
+
# `failure()` alone would MISS the case that matters most: a failing
|
|
196
|
+
# pytest step does not put the job in a failed state, because it is
|
|
197
|
+
# continue-on-error. Check those steps' own outcomes too, or a
|
|
198
|
+
# generator regression would be reported with no device logs to
|
|
199
|
+
# explain it. Runs before destroy, while the containers still exist.
|
|
200
|
+
if: >-
|
|
201
|
+
failure() ||
|
|
202
|
+
steps.srl-tests.outcome == 'failure' ||
|
|
203
|
+
steps.srl-sdk-verify.outcome == 'failure'
|
|
204
|
+
run: |
|
|
205
|
+
docker ps -a --format '{{.Names}}' | grep -e clab- || true
|
|
206
|
+
for c in $(docker ps -a --format '{{.Names}}' | grep -e clab- || true); do
|
|
207
|
+
echo "===== ${c} ====="
|
|
208
|
+
docker logs --tail 200 "$c" || true
|
|
209
|
+
done
|
|
210
|
+
|
|
211
|
+
- name: Destroy the lab
|
|
212
|
+
# Always: an abandoned privileged container keeps its port binding and
|
|
213
|
+
# its ~2 GB of disk until the runner is reclaimed.
|
|
214
|
+
if: always()
|
|
215
|
+
run: |
|
|
216
|
+
sudo -E containerlab destroy -t tests/srl/srl.clab.yml --cleanup || true
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a `v*` tag is pushed. Nothing here runs on a PR, so
|
|
4
|
+
# this workflow is deliberately NOT part of `ci-pr`'s `ci-gate` needs list
|
|
5
|
+
# (AGENTS.md: `main` requires only that one stable check name).
|
|
6
|
+
#
|
|
7
|
+
# Tagging without this workflow shipped nothing — the repo had no publish job
|
|
8
|
+
# at all, which is why v0.1.0 needed one written from scratch.
|
|
9
|
+
#
|
|
10
|
+
# One-time setup (PyPI side, not automatable from this repo):
|
|
11
|
+
# * Preferred: configure "Trusted Publishing" for this repo on PyPI. No
|
|
12
|
+
# secret is needed at all, and the `PYPI_TOKEN` environment reference
|
|
13
|
+
# below stays empty.
|
|
14
|
+
# * Otherwise create a PyPI API token scoped to this project and add it as
|
|
15
|
+
# the repository secret `PYPI_TOKEN`.
|
|
16
|
+
# Either way the `environment: pypi` gate below should be created in the
|
|
17
|
+
# repo settings so publishing requires an approver.
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
tags: ["v*"]
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
build:
|
|
27
|
+
runs-on: ubuntu-24.04
|
|
28
|
+
outputs:
|
|
29
|
+
version: ${{ steps.version.outputs.value }}
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: astral-sh/setup-uv@v5
|
|
33
|
+
|
|
34
|
+
# GitHub records a `run` step's outputs ONLY from `name=value` lines on
|
|
35
|
+
# stdout (or the deprecated `::set-output::`). A bare `print()` writes
|
|
36
|
+
# nothing to $GITHUB_OUTPUT, so `steps.version.outputs.value` below was
|
|
37
|
+
# always empty and the guard failed for EVERY tag -- including one that
|
|
38
|
+
# matched -- which is why the first release run (38891426993) failed with
|
|
39
|
+
# "pyproject.toml says " and an empty version rather than the real
|
|
40
|
+
# mismatch. Write the output explicitly, and fail here if it is empty
|
|
41
|
+
# instead of letting the comparison below treat "" as a version.
|
|
42
|
+
#
|
|
43
|
+
# `python3`, not `uv run`: tomllib is stdlib on the runner's 3.12, so the
|
|
44
|
+
# guard needs no synced environment, and the step then runs offline in
|
|
45
|
+
# `tests/test_matrix.py::test_release_tag_guard_*` without nesting uv.
|
|
46
|
+
- name: Read pyproject version
|
|
47
|
+
id: version
|
|
48
|
+
run: |
|
|
49
|
+
v="$(python3 -c 'import tomllib,pathlib;print(tomllib.loads(pathlib.Path("pyproject.toml").read_text()).get("project",{}).get("version",""))')"
|
|
50
|
+
if [ -z "$v" ]; then
|
|
51
|
+
echo "could not read [project].version from pyproject.toml" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
echo "value=$v" >> "$GITHUB_OUTPUT"
|
|
55
|
+
|
|
56
|
+
- name: Fail if the tag does not match pyproject
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
want="${GITHUB_REF_NAME#v}"
|
|
60
|
+
if [ "$want" != "${{ steps.version.outputs.value }}" ]; then
|
|
61
|
+
echo "tag $GITHUB_REF_NAME implies $want but pyproject.toml says ${{ steps.version.outputs.value }}" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
- run: uv build --out-dir dist
|
|
66
|
+
|
|
67
|
+
# --strict: fail on anything PyPI would reject, so a rejected upload is
|
|
68
|
+
# caught here rather than after the tag is already public.
|
|
69
|
+
- run: uv run --with twine twine check --strict dist/*
|
|
70
|
+
|
|
71
|
+
- uses: actions/upload-artifact@v4
|
|
72
|
+
with:
|
|
73
|
+
name: dist
|
|
74
|
+
path: dist/*
|
|
75
|
+
|
|
76
|
+
publish:
|
|
77
|
+
needs: build
|
|
78
|
+
runs-on: ubuntu-24.04
|
|
79
|
+
environment: pypi
|
|
80
|
+
permissions:
|
|
81
|
+
# id-token is what Trusted Publishing needs; harmless when a token is used.
|
|
82
|
+
id-token: write
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/download-artifact@v4
|
|
85
|
+
with:
|
|
86
|
+
name: dist
|
|
87
|
+
path: dist
|
|
88
|
+
# With no `password` this uses Trusted Publishing (OIDC). With
|
|
89
|
+
# PYPI_TOKEN set in the environment it falls back to that token. The
|
|
90
|
+
# value is passed via `env`, never interpolated into the `with:` block,
|
|
91
|
+
# so it cannot be echoed into a log.
|
|
92
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
93
|
+
env:
|
|
94
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
*.log
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv
|
|
12
|
+
.ruff_cache
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
|
|
15
|
+
# env files
|
|
16
|
+
*.env
|
|
17
|
+
|
|
18
|
+
# containerlab runtime state (LAB ONLY) — lives next to the topology file
|
|
19
|
+
tests/srl/clab-*/
|
|
20
|
+
|
|
21
|
+
# Temporary files
|
|
22
|
+
digest.txt
|
|
23
|
+
# Simulator snapshot payloads (run-local; strict golden comparison is follow-up)
|
|
24
|
+
tests/fixtures/golden/*.json
|
|
25
|
+
# Ephemeral workspace (yang_modules/, *_clients/, yang_tree/, verify/,
|
|
26
|
+
# model_gaps/). Never committed; every writer creates its own output dir
|
|
27
|
+
# on demand, so `rm -rf temp/` is always recoverable (re-download/recompile).
|
|
28
|
+
temp/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|