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.
Files changed (96) hide show
  1. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/docs.yml +4 -4
  2. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/e2e.yml +2 -2
  3. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/lint.yml +2 -2
  4. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/test.yml +2 -2
  5. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/CHANGELOG.md +43 -0
  6. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/PKG-INFO +4 -2
  7. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/README.md +3 -1
  8. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/commands.md +13 -0
  9. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/design.md +6 -2
  10. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/index.md +6 -4
  11. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/profiles.md +4 -0
  12. vcf_super_cli-0.6.0/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md +78 -0
  13. vcf_super_cli-0.6.0/docs/superpowers/specs/2026-07-02-vcf-super-cli-nsx-traceflow-design.md +107 -0
  14. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/pyproject.toml +1 -1
  15. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_builder.py +70 -0
  16. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_discover.py +43 -0
  17. vcf_super_cli-0.6.0/tests/test_pyvmomi_find.py +342 -0
  18. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/uv.lock +1 -1
  19. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/_version.py +1 -1
  20. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/builder.py +8 -1
  21. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/discover.py +4 -1
  22. vcf_super_cli-0.6.0/vsc/pyvmomi/find.py +187 -0
  23. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/inventory.py +60 -0
  24. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/skill/assets/SKILL.md +10 -1
  25. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.github/workflows/release.yml +0 -0
  26. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/.gitignore +0 -0
  27. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/AGENTS.md +0 -0
  28. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/LICENSE +0 -0
  29. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/install.md +0 -0
  30. {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
  31. {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
  32. {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
  33. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/usage.md +0 -0
  34. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/docs/writes.md +0 -0
  35. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/mkdocs.yml +0 -0
  36. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/__init__.py +0 -0
  37. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/README.md +0 -0
  38. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/__init__.py +0 -0
  39. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/conftest.py +0 -0
  40. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/e2e/test_read_smoke.py +0 -0
  41. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_cli.py +0 -0
  42. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete.py +0 -0
  43. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete_cache.py +0 -0
  44. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_complete_dynamic.py +0 -0
  45. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_config.py +0 -0
  46. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_errors.py +0 -0
  47. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_filter_pagination_cli.py +0 -0
  48. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_filters.py +0 -0
  49. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_paginate.py +0 -0
  50. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_params.py +0 -0
  51. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_preview.py +0 -0
  52. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_events.py +0 -0
  53. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_inventory.py +0 -0
  54. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_perf.py +0 -0
  55. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_pyvmomi_tasks.py +0 -0
  56. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_render.py +0 -0
  57. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_resources.py +0 -0
  58. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_session.py +0 -0
  59. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_skill.py +0 -0
  60. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_version.py +0 -0
  61. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_vmomi.py +0 -0
  62. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/tests/test_write_errors.py +0 -0
  63. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/__init__.py +0 -0
  64. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/__init__.py +0 -0
  65. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/app.py +0 -0
  66. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/profiles.py +0 -0
  67. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/cli/skill.py +0 -0
  68. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/__init__.py +0 -0
  69. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/schema.py +0 -0
  70. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/config/store.py +0 -0
  71. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/__init__.py +0 -0
  72. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/session.py +0 -0
  73. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/targets.py +0 -0
  74. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/connect/vmomi.py +0 -0
  75. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/__init__.py +0 -0
  76. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete.py +0 -0
  77. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_cache.py +0 -0
  78. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/complete_dynamic.py +0 -0
  79. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/filters.py +0 -0
  80. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/model.py +0 -0
  81. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/paginate.py +0 -0
  82. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/params.py +0 -0
  83. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/preview.py +0 -0
  84. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/gen/resources.py +0 -0
  85. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/logging_config.py +0 -0
  86. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/__init__.py +0 -0
  87. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/errors.py +0 -0
  88. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/exit_codes.py +0 -0
  89. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/output/render.py +0 -0
  90. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/__init__.py +0 -0
  91. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/events.py +0 -0
  92. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/perf.py +0 -0
  93. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/runner.py +0 -0
  94. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/pyvmomi/tasks.py +0 -0
  95. {vcf_super_cli-0.4.0 → vcf_super_cli-0.6.0}/vsc/skill/__init__.py +0 -0
  96. {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@v4
21
+ - uses: actions/checkout@v6
22
22
  - name: Install uv
23
- uses: astral-sh/setup-uv@v5
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@v3
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@v4
46
+ uses: actions/deploy-pages@v5
@@ -10,9 +10,9 @@ jobs:
10
10
  e2e:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v4
13
+ - uses: actions/checkout@v6
14
14
  - name: Install uv
15
- uses: astral-sh/setup-uv@v5
15
+ uses: astral-sh/setup-uv@v7
16
16
  with:
17
17
  enable-cache: true
18
18
  - name: Sync (dev)
@@ -13,9 +13,9 @@ jobs:
13
13
  lint:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v6
17
17
  - name: Install uv
18
- uses: astral-sh/setup-uv@v5
18
+ uses: astral-sh/setup-uv@v7
19
19
  with:
20
20
  enable-cache: true
21
21
  - name: Sync (dev)
@@ -17,9 +17,9 @@ jobs:
17
17
  matrix:
18
18
  python-version: ["3.12", "3.13"]
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v6
21
21
  - name: Install uv
22
- uses: astral-sh/setup-uv@v5
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.4.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 | planned |
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 | planned |
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` (a PropertyCollector property walk). All
72
- are reads no `--apply`.
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** — offline tab-completion (enums, formats, profiles, filter
23
- choices), per-field `--<field>` filter flags, and paging (`--all` /
24
- `--max-items` / `--limit`).
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.4.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