vcf-super-cli 0.5.0__tar.gz → 0.6.0__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.
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.github/workflows/docs.yml +4 -4
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.github/workflows/e2e.yml +2 -2
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.github/workflows/lint.yml +2 -2
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.github/workflows/test.yml +2 -2
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/CHANGELOG.md +24 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/PKG-INFO +2 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/README.md +1 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/commands.md +2 -0
- vcf_super_cli-0.6.0/docs/superpowers/specs/2026-07-02-vcf-super-cli-nsx-traceflow-design.md +107 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/pyproject.toml +1 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_builder.py +70 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_discover.py +43 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/uv.lock +1 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/_version.py +1 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/builder.py +8 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/discover.py +4 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/skill/assets/SKILL.md +4 -1
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.github/workflows/release.yml +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/.gitignore +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/AGENTS.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/LICENSE +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/design.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/index.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/install.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/profiles.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/usage.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/docs/writes.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/mkdocs.yml +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/e2e/README.md +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/e2e/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/e2e/conftest.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/e2e/test_read_smoke.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_cli.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_complete.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_complete_cache.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_complete_dynamic.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_config.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_errors.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_filter_pagination_cli.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_filters.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_paginate.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_params.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_preview.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_events.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_find.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_inventory.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_perf.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_tasks.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_render.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_resources.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_session.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_skill.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_version.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_vmomi.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/tests/test_write_errors.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/cli/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/cli/app.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/cli/profiles.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/cli/skill.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/config/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/config/schema.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/config/store.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/connect/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/connect/session.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/connect/targets.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/connect/vmomi.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/complete.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_cache.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_dynamic.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/filters.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/model.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/paginate.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/params.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/preview.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/gen/resources.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/logging_config.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/output/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/output/errors.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/output/exit_codes.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/output/render.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/events.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/find.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/inventory.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/perf.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/runner.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/tasks.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/skill/__init__.py +0 -0
- {vcf_super_cli-0.5.0 → vcf_super_cli-0.6.0}/vsc/skill/export.py +0 -0
|
@@ -18,9 +18,9 @@ jobs:
|
|
|
18
18
|
build:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
22
|
- name: Install uv
|
|
23
|
-
uses: astral-sh/setup-uv@
|
|
23
|
+
uses: astral-sh/setup-uv@v7
|
|
24
24
|
with:
|
|
25
25
|
enable-cache: true
|
|
26
26
|
- name: Sync (docs)
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
run: uv run mkdocs build --strict
|
|
30
30
|
- name: Upload pages artifact
|
|
31
31
|
if: github.ref == 'refs/heads/main'
|
|
32
|
-
uses: actions/upload-pages-artifact@
|
|
32
|
+
uses: actions/upload-pages-artifact@v5
|
|
33
33
|
with:
|
|
34
34
|
path: site
|
|
35
35
|
|
|
@@ -43,4 +43,4 @@ jobs:
|
|
|
43
43
|
steps:
|
|
44
44
|
- name: Deploy to GitHub Pages
|
|
45
45
|
id: deployment
|
|
46
|
-
uses: actions/deploy-pages@
|
|
46
|
+
uses: actions/deploy-pages@v5
|
|
@@ -17,9 +17,9 @@ jobs:
|
|
|
17
17
|
matrix:
|
|
18
18
|
python-version: ["3.12", "3.13"]
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
21
|
- name: Install uv
|
|
22
|
-
uses: astral-sh/setup-uv@
|
|
22
|
+
uses: astral-sh/setup-uv@v7
|
|
23
23
|
with:
|
|
24
24
|
enable-cache: true
|
|
25
25
|
- name: Set up Python ${{ matrix.python-version }}
|
|
@@ -7,6 +7,30 @@ versions may include breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## v0.6.0 — 2026-07-03
|
|
11
|
+
|
|
12
|
+
NSX Traceflow — inject a synthetic packet and read the exact path it takes through
|
|
13
|
+
the topology, the first tool for "why can't A reach B?".
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`vsc nsx traceflows` / `vsc nsx observations`** (#58) — NSX Policy Traceflow.
|
|
18
|
+
Inject a synthetic packet and read the exact path it takes through the topology —
|
|
19
|
+
the first tool for "why can't A reach B?". `traceflows` exposes the config surface
|
|
20
|
+
(`list`/`get`/`set`/`patch`/`delete` + the `policy-lm-restart-traceflow` action);
|
|
21
|
+
`set <id> --traceflow-config '<json>'` starts a trace (dry-run by default, `--apply`
|
|
22
|
+
to execute). `observations list <traceflow-id>` returns the traced path.
|
|
23
|
+
Pure allow-list addition — no generator changes; writes, JSON struct bodies and
|
|
24
|
+
paging come from the existing machinery. Manager API traceflow stays deferred.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `list --all` no longer crashes on operations that return a cursor-shaped result
|
|
29
|
+
but take no `cursor` input parameter (e.g. `nsx observations list`). Cursor
|
|
30
|
+
following is now guarded by whether the op actually accepts a `cursor`; for those
|
|
31
|
+
that don't, `--all` degrades to a safe single-page no-op instead of re-invoking
|
|
32
|
+
with a rejected `cursor` kwarg.
|
|
33
|
+
|
|
10
34
|
## v0.5.0 — 2026-06-08
|
|
11
35
|
|
|
12
36
|
Find VMs by attribute. Answers the everyday *"which VM has `10.20.3.41`?"* — a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vcf-super-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Modern, agent-friendly CLI for VMware Cloud Foundation 9, with a command tree generated dynamically from the vcf-sdk vAPI bindings.
|
|
5
5
|
Project-URL: Homepage, https://github.com/thomaschristory/vcf-super-cli
|
|
6
6
|
Project-URL: Documentation, https://thomaschristory.github.io/vcf-super-cli/
|
|
@@ -76,6 +76,7 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
|
|
|
76
76
|
| Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | ✅ v0.3 |
|
|
77
77
|
| Live resource-id completion | ✅ v0.4 |
|
|
78
78
|
| Find VMs by attribute — IP / hostname / MAC / guest OS / power (`vsc vsphere inventory find`) | ✅ v0.5 |
|
|
79
|
+
| NSX **Traceflow** — inject a synthetic packet, read its path (`vsc nsx traceflows` / `vsc nsx observations`) | ✅ v0.6 |
|
|
79
80
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
80
81
|
|
|
81
82
|
## Install
|
|
@@ -44,6 +44,7 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
|
|
|
44
44
|
| Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | ✅ v0.3 |
|
|
45
45
|
| Live resource-id completion | ✅ v0.4 |
|
|
46
46
|
| Find VMs by attribute — IP / hostname / MAC / guest OS / power (`vsc vsphere inventory find`) | ✅ v0.5 |
|
|
47
|
+
| NSX **Traceflow** — inject a synthetic packet, read its path (`vsc nsx traceflows` / `vsc nsx observations`) | ✅ v0.6 |
|
|
47
48
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
48
49
|
|
|
49
50
|
## Install
|
|
@@ -78,6 +78,8 @@ Generated from `vcf.nsx.policy`:
|
|
|
78
78
|
| `ip-pools` | `vsc nsx ip-pools set <id> --ip-address-pool '<json>' --apply` |
|
|
79
79
|
| `dhcp-server-configs` / `dhcp-relay-configs` | DHCP config reads + writes |
|
|
80
80
|
| `locale-services` | Tier-1 locale services reads + writes |
|
|
81
|
+
| `traceflows` | `vsc nsx traceflows set <id> --traceflow-config '<json>' --apply` (start a trace), `vsc nsx traceflows list` |
|
|
82
|
+
| `observations` | `vsc nsx observations list <traceflow-id>` (the traced packet path) |
|
|
81
83
|
|
|
82
84
|
## Curated commands
|
|
83
85
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# NSX Traceflow support
|
|
2
|
+
|
|
3
|
+
**Milestone:** v0.6 — NSX Traceflow · **Issue:** #58 · **Date:** 2026-07-02
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
NSX Traceflow injects a synthetic packet at a source port and reports the exact
|
|
8
|
+
path it takes through the logical and physical topology — the first tool you reach
|
|
9
|
+
for when debugging "why can't A talk to B?". The `vcf-nsx` SDK ships the Policy
|
|
10
|
+
Traceflow services, but `vsc nsx` doesn't expose them: the NSX surface is a curated
|
|
11
|
+
allow-list (`_NSX_SERVICE_SPECS` in `vsc/gen/discover.py`) and Traceflow isn't on it.
|
|
12
|
+
|
|
13
|
+
## Approach
|
|
14
|
+
|
|
15
|
+
Pure allow-list addition. The generator already handles everything Traceflow needs —
|
|
16
|
+
dry-run writes with `--apply`, JSON struct bodies, path-var mapping, cursor paging —
|
|
17
|
+
so no generator changes are required. Add two Policy service classes to the catalog
|
|
18
|
+
and let discovery produce the commands.
|
|
19
|
+
|
|
20
|
+
## Surface
|
|
21
|
+
|
|
22
|
+
Two Policy services (Manager API stays deferred, out of scope):
|
|
23
|
+
|
|
24
|
+
| Service class | Module | Group |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `Traceflows` | `vcf.nsx.policy.api.v1.infra_client` | `vsc nsx traceflows` |
|
|
27
|
+
| `Observations` | `vcf.nsx.policy.api.v1.infra.traceflows_client` | `vsc nsx observations` |
|
|
28
|
+
|
|
29
|
+
### `vsc nsx traceflows`
|
|
30
|
+
|
|
31
|
+
| Verb | HTTP | Notes |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `list` | GET | list traceflow configs; gets `--all`/`--max-items`/`--limit` |
|
|
34
|
+
| `get <traceflow-id>` | GET | read one config |
|
|
35
|
+
| `set <traceflow-id> --traceflow-config '<json>'` | PUT | create/replace a config (start a trace) |
|
|
36
|
+
| `patch <traceflow-id> --traceflow-config '<json>'` | PATCH | partial update |
|
|
37
|
+
| `delete <traceflow-id>` | DELETE | remove a config |
|
|
38
|
+
| `policy-lm-restart-traceflow <traceflow-id>` | POST | restart action |
|
|
39
|
+
|
|
40
|
+
### `vsc nsx observations`
|
|
41
|
+
|
|
42
|
+
| Verb | HTTP | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `list <traceflow-id>` | GET | the traced packet path (the actual result) |
|
|
45
|
+
|
|
46
|
+
## Behavior (inherited from existing infra — no new code)
|
|
47
|
+
|
|
48
|
+
- **Dry-run by default.** `set`/`patch`/`delete`/restart preview the resolved request
|
|
49
|
+
and open no connection; `--apply` executes.
|
|
50
|
+
- **`--traceflow-config` is a STRUCT** → accepts a JSON blob, validated client-side
|
|
51
|
+
before any connection. Malformed/incomplete JSON → clean usage error (exit 2).
|
|
52
|
+
- **Paging.** `traceflows list` is cursor-paginated (`--all` / `--max-items` /
|
|
53
|
+
`--limit`). `observations list` returns a cursor-shaped result but takes no
|
|
54
|
+
`cursor` *input*, so `--all` degrades to a safe single-page no-op (see the
|
|
55
|
+
generator guard added in review below) rather than re-invoking with a cursor.
|
|
56
|
+
- **Read contract holds.** Under `read_only=True` both services yield only `get`/`list`
|
|
57
|
+
GETs, preserving the invariant asserted by `test_expanded_catalog_read_contract_holds`.
|
|
58
|
+
|
|
59
|
+
## Accepted quirks (deliberate — no special-casing)
|
|
60
|
+
|
|
61
|
+
- `observations` is its own top-level group, not nested under `traceflows`. This is
|
|
62
|
+
how the generic generator names groups (by service short name); nesting would need
|
|
63
|
+
a per-service override the generator doesn't have.
|
|
64
|
+
- The restart verb is the verbose `policy-lm-restart-traceflow` (POST with no
|
|
65
|
+
`?action=`, so the op id is kebab-cased). Renaming is a separate cross-cutting
|
|
66
|
+
concern, not part of this issue.
|
|
67
|
+
|
|
68
|
+
## Testing
|
|
69
|
+
|
|
70
|
+
Extend `tests/test_discover.py` (offline, against the real installed SDK):
|
|
71
|
+
|
|
72
|
+
1. `nsx_services()` includes `Traceflows` and `Observations`.
|
|
73
|
+
2. `traceflows` discovery yields `list`/`get`/`set`/`patch`/`delete` with the right
|
|
74
|
+
HTTP methods, and `--traceflow-config` is a required STRUCT body on `set`.
|
|
75
|
+
3. `observations` discovery yields a `list` whose required `traceflow_id` is a path
|
|
76
|
+
param.
|
|
77
|
+
4. The read-only contract still holds across the expanded catalog (existing test
|
|
78
|
+
covers the new services automatically).
|
|
79
|
+
|
|
80
|
+
Builder-level (`tests/test_builder.py` style): `traceflows set` without `--apply`
|
|
81
|
+
emits a dry-run request plan and opens no connection.
|
|
82
|
+
|
|
83
|
+
## Docs
|
|
84
|
+
|
|
85
|
+
- README scope table: add a Traceflow row (or fold into NSX Policy).
|
|
86
|
+
- `docs/commands.md`: add `traceflows` / `observations` to the NSX table with a
|
|
87
|
+
create→observe example.
|
|
88
|
+
- `vsc/skill/assets/SKILL.md`: add the two groups to the NSX group list.
|
|
89
|
+
- `CHANGELOG.md`: Added entry under Unreleased.
|
|
90
|
+
|
|
91
|
+
## Out of scope
|
|
92
|
+
|
|
93
|
+
- Manager API traceflow (`vcf.nsx.api.v1`).
|
|
94
|
+
- Group-nesting / verb-renaming ergonomics (separate issue if wanted).
|
|
95
|
+
|
|
96
|
+
## Review follow-ups (adversarial pass)
|
|
97
|
+
|
|
98
|
+
- **Critical (fixed):** `observations list --all` crashed — the op returns a
|
|
99
|
+
cursor-shaped result but takes no `cursor` input, so `follow_cursor` re-invoked
|
|
100
|
+
with `cursor=…` and the SDK method raised `TypeError`. Fixed generically in
|
|
101
|
+
`_run_list` via a `supports_cursor` guard (only follow when the op has a `cursor`
|
|
102
|
+
input param); `--all` now no-ops for such ops, matching plain-list behaviour.
|
|
103
|
+
- **Docs (fixed):** `observations list`'s `traceflow_id` is a path variable →
|
|
104
|
+
positional argument (`list <traceflow-id>`), not a `--traceflow-id` option.
|
|
105
|
+
- **Coverage (added):** restart verb asserted in discovery (POST) and its dry-run
|
|
106
|
+
gate covered at builder level; explicit `cli_verb == "list"` assertion for
|
|
107
|
+
observations; regression test for the `--all` no-op.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vcf-super-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Modern, agent-friendly CLI for VMware Cloud Foundation 9, with a command tree generated dynamically from the vcf-sdk vAPI bindings."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -259,6 +259,76 @@ def test_write_apply_routes_named_body_op() -> None:
|
|
|
259
259
|
assert "segment" in _CAPTURED # body struct routed to the SDK method
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
def test_traceflows_set_dry_run_emits_put_body_and_never_connects() -> None:
|
|
263
|
+
# #58: the new NSX Traceflow write surface must inherit the dry-run gate —
|
|
264
|
+
# preview a PUT with the traceflow-config body, opening no connection.
|
|
265
|
+
calls: list[str] = []
|
|
266
|
+
|
|
267
|
+
def connect(backend: str) -> object:
|
|
268
|
+
calls.append(backend)
|
|
269
|
+
return object()
|
|
270
|
+
|
|
271
|
+
op = _write("Traceflows", "set", "nsx")
|
|
272
|
+
app = _write_app(op, object, connect)
|
|
273
|
+
result = runner.invoke(app, ["tf-1", "--traceflow-config", '{"display_name": "tf-web"}'])
|
|
274
|
+
assert result.exit_code == 0, result.stdout
|
|
275
|
+
env = json.loads(result.stdout)
|
|
276
|
+
assert env["applied"] is False
|
|
277
|
+
assert env["request"]["method"] == "PUT"
|
|
278
|
+
assert env["request"]["body"] == {"display_name": "tf-web"}
|
|
279
|
+
assert calls == [] # invariant: dry-run opens no connection
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_traceflows_restart_dry_run_gate_never_connects() -> None:
|
|
283
|
+
# #58: the restart action (POST, no ?action=) is a write and must inherit the
|
|
284
|
+
# dry-run gate — preview and open no connection without --apply.
|
|
285
|
+
calls: list[str] = []
|
|
286
|
+
|
|
287
|
+
def connect(backend: str) -> object:
|
|
288
|
+
calls.append(backend)
|
|
289
|
+
return object()
|
|
290
|
+
|
|
291
|
+
op = _write("Traceflows", "policy-lm-restart-traceflow", "nsx")
|
|
292
|
+
app = _write_app(op, object, connect)
|
|
293
|
+
result = runner.invoke(app, ["tf-1"]) # no --apply
|
|
294
|
+
assert result.exit_code == 0, result.stdout
|
|
295
|
+
env = json.loads(result.stdout)
|
|
296
|
+
assert env["applied"] is False
|
|
297
|
+
assert env["request"]["method"] == "POST"
|
|
298
|
+
assert calls == [] # invariant: dry-run opens no connection
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_observations_list_all_on_non_cursor_op_does_not_crash() -> None:
|
|
302
|
+
# #58: observations list returns a cursor-shaped result but the op takes NO
|
|
303
|
+
# cursor input param. --all must not re-invoke with a cursor kwarg (which the
|
|
304
|
+
# SDK method rejects) — it degrades to a safe single-page no-op, like vSphere.
|
|
305
|
+
invocations: list[dict[str, object]] = []
|
|
306
|
+
|
|
307
|
+
class Res:
|
|
308
|
+
def __init__(self, cursor: str | None) -> None:
|
|
309
|
+
self.results = [{"hop": 1}]
|
|
310
|
+
self.cursor = cursor
|
|
311
|
+
|
|
312
|
+
class FakeObs:
|
|
313
|
+
def __init__(self, _cfg: object) -> None:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
def policy_lm_list_traceflow_observations(self, **kwargs: object) -> Res:
|
|
317
|
+
invocations.append(kwargs)
|
|
318
|
+
if "cursor" in kwargs:
|
|
319
|
+
raise TypeError("unexpected keyword argument 'cursor'")
|
|
320
|
+
return Res("PAGE2") # server hands back a cursor
|
|
321
|
+
|
|
322
|
+
obs = next(c for c in nsx_services() if c.__name__ == "Observations")
|
|
323
|
+
op = next(o for o in discover_operations(obs, "nsx") if o.cli_verb == "list")
|
|
324
|
+
app = _app_for(op, FakeObs)
|
|
325
|
+
result = runner.invoke(app, ["tf-1", "--all"])
|
|
326
|
+
assert result.exit_code == 0, result.stdout or repr(result.exception)
|
|
327
|
+
# The op takes no cursor input, so --all must invoke exactly once and never
|
|
328
|
+
# forward a cursor kwarg — no attempt to follow the returned cursor.
|
|
329
|
+
assert invocations == [{"traceflow_id": "tf-1"}]
|
|
330
|
+
|
|
331
|
+
|
|
262
332
|
def _synthetic_write_with_param(param_name: str) -> Operation:
|
|
263
333
|
return Operation(
|
|
264
334
|
backend="vsphere",
|
|
@@ -159,6 +159,49 @@ def test_expanded_catalog_read_contract_holds() -> None:
|
|
|
159
159
|
assert op.cli_verb in {"get", "list"}
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
# --------------------------------------------------------------------------- #
|
|
163
|
+
# NSX Traceflow (#58)
|
|
164
|
+
# --------------------------------------------------------------------------- #
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_nsx_catalog_includes_traceflow_services() -> None:
|
|
168
|
+
names = {c.__name__ for c in nsx_services()}
|
|
169
|
+
assert {"Traceflows", "Observations"} <= names
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _traceflow_ops() -> list[Operation]:
|
|
173
|
+
tf = next(c for c in nsx_services() if c.__name__ == "Traceflows")
|
|
174
|
+
return discover_operations(tf, "nsx", read_only=False)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_traceflows_expose_crud_verbs() -> None:
|
|
178
|
+
by_verb = {o.cli_verb: o for o in _traceflow_ops()}
|
|
179
|
+
assert by_verb["list"].http_method == "GET"
|
|
180
|
+
assert by_verb["get"].http_method == "GET"
|
|
181
|
+
assert by_verb["set"].http_method == "PUT"
|
|
182
|
+
assert by_verb["patch"].http_method == "PATCH"
|
|
183
|
+
assert by_verb["delete"].http_method == "DELETE"
|
|
184
|
+
# The restart action is a POST with no ?action=, so it keeps its kebab op id.
|
|
185
|
+
assert by_verb["policy-lm-restart-traceflow"].http_method == "POST"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_traceflows_set_body_is_required_struct() -> None:
|
|
189
|
+
set_op = next(o for o in _traceflow_ops() if o.cli_verb == "set")
|
|
190
|
+
cfg = next(p for p in set_op.params if p.name == "traceflow_config")
|
|
191
|
+
assert cfg.kind is ParamKind.STRUCT
|
|
192
|
+
assert cfg.required
|
|
193
|
+
assert cfg.is_body
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_observations_list_has_required_traceflow_id_path_param() -> None:
|
|
197
|
+
obs = next(c for c in nsx_services() if c.__name__ == "Observations")
|
|
198
|
+
ops = discover_operations(obs, "nsx")
|
|
199
|
+
assert [o.cli_verb for o in ops] == ["list"] # single read op, verb is 'list'
|
|
200
|
+
tid = next(p for p in ops[0].params if p.name == "traceflow_id")
|
|
201
|
+
assert tid.in_path # -> positional CLI argument, not a --traceflow-id option
|
|
202
|
+
assert tid.required
|
|
203
|
+
|
|
204
|
+
|
|
162
205
|
def test_action_from_url_edge_cases() -> None:
|
|
163
206
|
assert _action_from_url("/x?action=revise") == "revise"
|
|
164
207
|
assert _action_from_url("/x?force=true&action=reprocess") == "reprocess" # not first
|
|
@@ -295,6 +295,7 @@ def make_command(op: Operation, connect_fn: ConnectFn) -> Callable[..., None]:
|
|
|
295
295
|
all_flag=bool(kwargs.get(_ALL_PARAM, False)),
|
|
296
296
|
max_items=kwargs.get(_MAXITEMS_PARAM),
|
|
297
297
|
limit=kwargs.get(_LIMIT_PARAM),
|
|
298
|
+
supports_cursor=any(p.name == "cursor" for p in op.params),
|
|
298
299
|
)
|
|
299
300
|
emit(result, fmt)
|
|
300
301
|
elif op.is_write:
|
|
@@ -366,16 +367,22 @@ def _run_list(
|
|
|
366
367
|
all_flag: bool,
|
|
367
368
|
max_items: int | None,
|
|
368
369
|
limit: int | None,
|
|
370
|
+
supports_cursor: bool = True,
|
|
369
371
|
) -> Any:
|
|
370
372
|
"""Execute a list operation, applying ``--all`` / ``--max-items`` / ``--limit``.
|
|
371
373
|
|
|
372
374
|
Without any paging flag the raw result is returned unchanged (preserving the
|
|
373
375
|
cursor for manual paging). ``--all`` follows the cursor across pages; the caps
|
|
374
376
|
slice the returned items client-side.
|
|
377
|
+
|
|
378
|
+
``supports_cursor`` guards cursor-following: a few ops (e.g. NSX traceflow
|
|
379
|
+
observations) return a cursor-shaped result yet take no ``cursor`` *input*, so
|
|
380
|
+
re-invoking with a cursor would raise. For those ``--all`` degrades to a safe
|
|
381
|
+
single-page no-op, mirroring how it already no-ops on plain (vSphere) lists.
|
|
375
382
|
"""
|
|
376
383
|
first = invoke(**sdk_kwargs)
|
|
377
384
|
if _is_cursor_list(first):
|
|
378
|
-
if all_flag:
|
|
385
|
+
if all_flag and supports_cursor:
|
|
379
386
|
# Seed the loop with the page we already fetched (no double-fetch).
|
|
380
387
|
def fetch_page(cursor: str | None) -> tuple[list[Any], str | None]:
|
|
381
388
|
res = first if cursor is None else invoke(**{**sdk_kwargs, "cursor": cursor})
|
|
@@ -184,7 +184,8 @@ def vsphere_services() -> list[type]:
|
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
# NSX Policy services. Imported defensively so a single moved symbol cannot break
|
|
187
|
-
# the whole NSX surface. v0.2 adds IP pools, DHCP configs and Tier-1 locale services
|
|
187
|
+
# the whole NSX surface. v0.2 adds IP pools, DHCP configs and Tier-1 locale services;
|
|
188
|
+
# #58 adds Traceflow (config CRUD + restart) and its observations (the traced path).
|
|
188
189
|
_NSX_SERVICE_SPECS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
189
190
|
(
|
|
190
191
|
"vcf.nsx.policy.api.v1.infra_client",
|
|
@@ -196,6 +197,7 @@ _NSX_SERVICE_SPECS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
196
197
|
"IpPools",
|
|
197
198
|
"DhcpServerConfigs",
|
|
198
199
|
"DhcpRelayConfigs",
|
|
200
|
+
"Traceflows",
|
|
199
201
|
),
|
|
200
202
|
),
|
|
201
203
|
(
|
|
@@ -203,6 +205,7 @@ _NSX_SERVICE_SPECS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
203
205
|
("Groups", "SecurityPolicies", "GatewayPolicies"),
|
|
204
206
|
),
|
|
205
207
|
("vcf.nsx.policy.api.v1.infra.tier_1s_client", ("LocaleServices",)),
|
|
208
|
+
("vcf.nsx.policy.api.v1.infra.traceflows_client", ("Observations",)),
|
|
206
209
|
)
|
|
207
210
|
|
|
208
211
|
|
|
@@ -12,7 +12,10 @@ the `vcf-sdk` vAPI bindings and split into two product groups:
|
|
|
12
12
|
resource-pool, and the VM power/hardware leaves: power, cpu, memory, disk, ethernet —
|
|
13
13
|
each takes the VM id as an argument, e.g. `vsc vsphere power stop <vm>`)
|
|
14
14
|
- `vsc nsx …` — NSX Policy (segments, tier0s, tier1s, services, groups, security-policies,
|
|
15
|
-
gateway-policies, ip-pools, dhcp-server-configs, dhcp-relay-configs, locale-services
|
|
15
|
+
gateway-policies, ip-pools, dhcp-server-configs, dhcp-relay-configs, locale-services,
|
|
16
|
+
traceflows, observations — Traceflow injects a synthetic packet; `traceflows set <id>
|
|
17
|
+
--traceflow-config '<json>'` starts one and `observations list <traceflow-id>`
|
|
18
|
+
reads the path it took)
|
|
16
19
|
|
|
17
20
|
Discover the live surface with `vsc --help`, `vsc vsphere --help`, `vsc nsx --help`,
|
|
18
21
|
and `vsc vsphere vm --help`. Leaves expose `list`/`get <id>` reads and — where the SDK
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|