vcf-super-cli 0.4.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.4.0 → vcf_super_cli-0.6.0}/.github/workflows/docs.yml +4 -4
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/e2e.yml +2 -2
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/lint.yml +2 -2
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/test.yml +2 -2
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/CHANGELOG.md +43 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/PKG-INFO +4 -2
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/README.md +3 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/commands.md +13 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/design.md +6 -2
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/index.md +6 -4
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/profiles.md +4 -0
- vcf_super_cli-0.6.0/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md +78 -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.4.0 → vcf_super_cli-0.6.0}/pyproject.toml +1 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_builder.py +70 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_discover.py +43 -0
- vcf_super_cli-0.6.0/tests/test_pyvmomi_find.py +342 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/uv.lock +1 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/_version.py +1 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/builder.py +8 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/discover.py +4 -1
- vcf_super_cli-0.6.0/vsc/pyvmomi/find.py +187 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/inventory.py +60 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/skill/assets/SKILL.md +10 -1
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/release.yml +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.gitignore +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/AGENTS.md +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/LICENSE +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/install.md +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md +0 -0
- {vcf_super_cli-0.4.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.4.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.4.0 → vcf_super_cli-0.6.0}/docs/usage.md +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/writes.md +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/mkdocs.yml +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/README.md +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/conftest.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/test_read_smoke.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_cli.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete_cache.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete_dynamic.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_config.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_errors.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_filter_pagination_cli.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_filters.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_paginate.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_params.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_preview.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_events.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_inventory.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_perf.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_tasks.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_render.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_resources.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_session.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_skill.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_version.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_vmomi.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_write_errors.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/app.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/profiles.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/skill.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/schema.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/store.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/session.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/targets.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/vmomi.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_cache.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_dynamic.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/filters.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/model.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/paginate.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/params.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/preview.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/resources.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/logging_config.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/errors.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/exit_codes.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/render.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/__init__.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/events.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/perf.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/runner.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/tasks.py +0 -0
- {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/skill/__init__.py +0 -0
- {vcf_super_cli-0.4.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,49 @@ 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
|
+
|
|
34
|
+
## v0.5.0 — 2026-06-08
|
|
35
|
+
|
|
36
|
+
Find VMs by attribute. Answers the everyday *"which VM has `10.20.3.41`?"* — a
|
|
37
|
+
reverse lookup the REST `vm list` filter can't do, because guest networking lives
|
|
38
|
+
only on the pyVmomi `guest.*` properties.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **`vsc vsphere inventory find`** (#54) — locate VMs by guest/runtime attribute
|
|
43
|
+
without knowing the moid: `--ip` (exact or CIDR, IPv4/IPv6, across the primary and
|
|
44
|
+
every NIC address), `--name`, `--hostname`, `--guest-os` (case-insensitive
|
|
45
|
+
substring or glob), `--mac` (exact), and `--power-state`. Flags **AND** together; a
|
|
46
|
+
repeated flag **ORs** within its field; at least one match flag is required (it
|
|
47
|
+
refuses to dump the whole inventory). `--props` (repeatable) widens each hit's
|
|
48
|
+
output only — never a match criterion. One PropertyCollector round-trip over a
|
|
49
|
+
container view (always destroyed); a pure, pyVmomi-free matcher does the filtering.
|
|
50
|
+
Reads only — no `--apply` — and emits the same JSON / error envelope / exit codes
|
|
51
|
+
as the other pyVmomi fallback commands.
|
|
52
|
+
|
|
10
53
|
## v0.4.0 — 2026-06-08
|
|
11
54
|
|
|
12
55
|
Live resource-id completion. Pressing `<TAB>` on an id-typed argument can now
|
|
@@ -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/
|
|
@@ -74,7 +74,9 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
|
|
|
74
74
|
| NSX **Policy API** read (`vsc nsx …`) | ✅ v0.1 |
|
|
75
75
|
| Writes — dry-run by default + `--apply` (`vsc vsphere …` / `vsc nsx …`) | ✅ v0.2 |
|
|
76
76
|
| Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | ✅ v0.3 |
|
|
77
|
-
| Live resource-id completion |
|
|
77
|
+
| Live resource-id completion | ✅ v0.4 |
|
|
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 |
|
|
78
80
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
79
81
|
|
|
80
82
|
## Install
|
|
@@ -42,7 +42,9 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
|
|
|
42
42
|
| NSX **Policy API** read (`vsc nsx …`) | ✅ v0.1 |
|
|
43
43
|
| Writes — dry-run by default + `--apply` (`vsc vsphere …` / `vsc nsx …`) | ✅ v0.2 |
|
|
44
44
|
| Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | ✅ v0.3 |
|
|
45
|
-
| Live resource-id completion |
|
|
45
|
+
| Live resource-id completion | ✅ v0.4 |
|
|
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 |
|
|
46
48
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
47
49
|
|
|
48
50
|
## Install
|
|
@@ -51,6 +51,17 @@ API. Those commands live under `vsc vsphere` alongside the generated ones, are
|
|
|
51
51
|
managed-object properties the REST list ops omit (device tree, custom attributes),
|
|
52
52
|
via the PropertyCollector. `--props` is repeatable (e.g. `--props config.hardware`);
|
|
53
53
|
with none, a small per-type summary set is returned.
|
|
54
|
+
- `vsc vsphere inventory find [--ip …] [--name …] [--hostname …] [--mac …] [--guest-os …] [--power-state …] [--props …]`
|
|
55
|
+
— **find VMs by guest/runtime attribute without knowing the moid.** This is the
|
|
56
|
+
answer to *"which VM has `10.20.3.41`?"*: guest networking isn't in the REST
|
|
57
|
+
`vm list` filter, so this sweeps every VM's `guest.*` properties in one round-trip.
|
|
58
|
+
`--ip` accepts an exact address or a CIDR (`10.20.3.0/24`, IPv4 or IPv6) and matches
|
|
59
|
+
across the primary and every NIC address; `--name`/`--hostname`/`--guest-os` are
|
|
60
|
+
case-insensitive substring or glob; `--mac` is exact; `--power-state` is one of
|
|
61
|
+
`poweredOn`/`poweredOff`/`suspended`. Flags **AND** together, a repeated flag **ORs**
|
|
62
|
+
within its field, and at least one match flag is required. `--props` (repeatable)
|
|
63
|
+
widens each hit's output only — it never affects matching. Powered-off VMs and those
|
|
64
|
+
without VMware Tools report no guest IP and won't match `--ip`.
|
|
54
65
|
|
|
55
66
|
## `vsc nsx …` (NSX Policy)
|
|
56
67
|
|
|
@@ -67,6 +78,8 @@ Generated from `vcf.nsx.policy`:
|
|
|
67
78
|
| `ip-pools` | `vsc nsx ip-pools set <id> --ip-address-pool '<json>' --apply` |
|
|
68
79
|
| `dhcp-server-configs` / `dhcp-relay-configs` | DHCP config reads + writes |
|
|
69
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) |
|
|
70
83
|
|
|
71
84
|
## Curated commands
|
|
72
85
|
|
|
@@ -68,8 +68,11 @@ SOAP API. Those are **hand-written read-only commands** mounted under
|
|
|
68
68
|
and exit codes (`vsc/pyvmomi/runner.py`).
|
|
69
69
|
|
|
70
70
|
Commands: `perf` (PerformanceManager counters), `events` / `tasks`
|
|
71
|
-
(Event/Task managers), and `inventory`
|
|
72
|
-
|
|
71
|
+
(Event/Task managers), and `inventory` — a PropertyCollector property walk
|
|
72
|
+
(`inventory vm`/`host`) plus `inventory find`, which sweeps every VM's `guest.*`
|
|
73
|
+
properties in one round-trip to locate a VM by IP / name / hostname / MAC / guest
|
|
74
|
+
OS / power state (the fields the REST `vm list` filter can't reach). All are
|
|
75
|
+
reads — no `--apply`.
|
|
73
76
|
|
|
74
77
|
## Design specs
|
|
75
78
|
|
|
@@ -78,6 +81,7 @@ The authoritative, milestone-by-milestone designs live in the repository:
|
|
|
78
81
|
- [v0.1 — dynamic read-only CLI](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md)
|
|
79
82
|
- [v0.2 — writes](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md)
|
|
80
83
|
- [v0.3 — ergonomics](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md)
|
|
84
|
+
- [v0.5 — find VMs by attribute](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md)
|
|
81
85
|
|
|
82
86
|
## Contracts
|
|
83
87
|
|
|
@@ -19,11 +19,13 @@ $ vsc vsphere perf vm vm-42 --metric cpu.usage
|
|
|
19
19
|
|
|
20
20
|
- **Mirrors the real API** — commands come from the SDK's vAPI metadata, covering
|
|
21
21
|
vCenter and NSX from one generator.
|
|
22
|
-
- **Ergonomic** —
|
|
23
|
-
|
|
24
|
-
`--max-items` /
|
|
22
|
+
- **Ergonomic** — tab-completion (enums, formats, profiles, filter choices, and
|
|
23
|
+
opt-in [live resource ids](usage.md#live-resource-id-completion-opt-in)),
|
|
24
|
+
per-field `--<field>` filter flags, and paging (`--all` / `--max-items` /
|
|
25
|
+
`--limit`).
|
|
25
26
|
- **pyVmomi fallback** — read-only `perf`, `events`, `tasks`, and `inventory`
|
|
26
|
-
commands for areas the REST/vAPI surface doesn't cover
|
|
27
|
+
commands for areas the REST/vAPI surface doesn't cover, including
|
|
28
|
+
`inventory find` to locate a VM by IP / hostname / MAC / guest OS.
|
|
27
29
|
- **Safe by default** — writes are dry-run unless `--apply`; a dry-run never connects.
|
|
28
30
|
- **Agent-friendly** — JSON output, stable error envelope, documented exit codes,
|
|
29
31
|
bundled agent Skill.
|
|
@@ -56,6 +56,10 @@ VSC_PROFILE=prod vsc nsx segments list
|
|
|
56
56
|
| `VSC_VSPHERE_INSECURE` / `VSC_NSX_INSECURE` | `1`/`true` to skip TLS verification (lab/self-signed) |
|
|
57
57
|
| `VSC_CONFIG_FILE` | Override the config file location |
|
|
58
58
|
| `VSC_LOG_LEVEL` | Log level on stderr (default `WARNING`) |
|
|
59
|
+
| `VSC_COMPLETE_DYNAMIC` | `1`/`true` to enable [live resource-id completion](usage.md#live-resource-id-completion-opt-in) (off by default) |
|
|
60
|
+
| `VSC_COMPLETE_TTL` | Live-completion cache TTL in seconds (default `60`) |
|
|
61
|
+
| `VSC_COMPLETE_TIMEOUT` | Hard timeout for a live-completion fetch in seconds (default `2`) |
|
|
62
|
+
| `VSC_CACHE_DIR` | Override the cache directory (where live-completion results are cached) |
|
|
59
63
|
|
|
60
64
|
!!! warning "TLS"
|
|
61
65
|
Verification is **on by default**. Only set `VSC_*_INSECURE` (or
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# v0.5 — find VMs by attribute
|
|
2
|
+
|
|
3
|
+
**Milestone:** v0.5 — find VMs · **Issue:** #54 · **Date:** 2026-06-08
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
You can `vsc vsphere vm list` and filter on what the vAPI `VM.FilterSpec` exposes
|
|
8
|
+
(names, power states, hosts, clusters, datacenters, folders, resource pools).
|
|
9
|
+
None of the **guest/runtime network fields** are filterable there, so the everyday
|
|
10
|
+
question *"which VM has `10.20.3.41`?"* is unanswerable from the CLI. Guest
|
|
11
|
+
networking lives only on the pyVmomi `guest.*` properties; the per-VM REST guest
|
|
12
|
+
API needs the moid first, useless for a reverse lookup.
|
|
13
|
+
|
|
14
|
+
## Command
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
vsc vsphere inventory find [--ip ...] [--name ...] [--hostname ...] [--mac ...]
|
|
18
|
+
[--guest-os ...] [--power-state ...] [--props ...] [-o ...]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Third command under `inventory_app`, next to `vm`/`host`. VM-focused for v1.
|
|
22
|
+
|
|
23
|
+
### Match semantics
|
|
24
|
+
|
|
25
|
+
- Flags **AND** together; a repeated flag **ORs** within that field.
|
|
26
|
+
- `--ip` matches any address across `guest.ipAddress` + every `guest.net[].ipAddress`,
|
|
27
|
+
as an **exact IP or CIDR** via stdlib `ipaddress` (IPv4 and IPv6). An exact IP is
|
|
28
|
+
just a `/32` (or `/128`) network, so one code path covers both.
|
|
29
|
+
- `--name`, `--hostname`, `--guest-os` are case-insensitive **substring**, or
|
|
30
|
+
**glob** when the pattern has `*?[`. `--name` is fully client-side (one matcher).
|
|
31
|
+
- `--mac` exact, case-insensitive, against `guest.net[].macAddress`.
|
|
32
|
+
- `--power-state` ∈ {poweredOn, poweredOff, suspended} against `runtime.powerState`.
|
|
33
|
+
- **No match flag → usage error** (exit 2); we refuse to dump the whole inventory.
|
|
34
|
+
`--props`/`-o` alone do not count.
|
|
35
|
+
|
|
36
|
+
### `--props` passthrough (output only)
|
|
37
|
+
|
|
38
|
+
Repeatable; mirrors `inventory vm --props`. Appends arbitrary property paths to the
|
|
39
|
+
single PropertyCollector retrieve so each **matched** VM also surfaces them under a
|
|
40
|
+
`properties` sub-dict — bridging search→inspect in one call. Never a match criterion.
|
|
41
|
+
|
|
42
|
+
## Implementation
|
|
43
|
+
|
|
44
|
+
`vsc/pyvmomi/find.py`:
|
|
45
|
+
|
|
46
|
+
- **Pure matcher** (`Criteria`, `matches`, `summarize`, `validate_criteria`, plus
|
|
47
|
+
`_ip_match`/`_text_match`/`_addresses`/`_macs`) — operates only on a plain
|
|
48
|
+
props dict, no pyVmomi import in the hot path, so it unit-tests in isolation.
|
|
49
|
+
- **One round-trip** (`find_matches` → `_retrieve_all_vms`): a
|
|
50
|
+
`CreateContainerView(rootFolder, [vim.VirtualMachine], recursive=True)` plus a
|
|
51
|
+
single `RetrieveContents` with a `TraversalSpec` pulling the fixed search paths
|
|
52
|
+
**plus** any `--props`. The container view is **always destroyed** in a `finally`.
|
|
53
|
+
|
|
54
|
+
`vsc/pyvmomi/inventory.py` adds the thin `find` command: builds `Criteria` from the
|
|
55
|
+
flags, rejects an empty criteria set and malformed `--ip`/`--power-state` as usage
|
|
56
|
+
errors, and emits through the shared `run_read` runner (same JSON / error envelope /
|
|
57
|
+
exit codes as the other pyVmomi reads).
|
|
58
|
+
|
|
59
|
+
### Per-hit shape
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "obj": {"type": "VirtualMachine", "value": "vm-101"},
|
|
63
|
+
"name": "web-1", "power_state": "poweredOn",
|
|
64
|
+
"ip_addresses": ["10.20.3.41"], "hostname": "web-1.corp",
|
|
65
|
+
"guest_os": "Ubuntu Linux (64-bit)",
|
|
66
|
+
"properties": { "config.version": "vmx-19" } } // only with --props
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
No matches → empty array, exit 0.
|
|
70
|
+
|
|
71
|
+
## Scope / non-goals (v1)
|
|
72
|
+
|
|
73
|
+
- vSphere/pyVmomi only; NSX has its own search surface.
|
|
74
|
+
- VMs only (no host/network/datastore search yet).
|
|
75
|
+
- Single `RetrieveContents` (no paging). Very large inventories may later want
|
|
76
|
+
`RetrievePropertiesEx` + `maxObjects` paging — a scaling follow-up.
|
|
77
|
+
- VMs without VMware Tools / powered-off report no (or stale) guest IP and won't
|
|
78
|
+
match `--ip`; expected, noted in the docs.
|
|
@@ -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
|