vcf-super-cli 0.3.0__tar.gz → 0.5.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.3.0 → vcf_super_cli-0.5.0}/CHANGELOG.md +48 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/PKG-INFO +3 -2
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/README.md +2 -1
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/commands.md +11 -0
- vcf_super_cli-0.5.0/docs/design.md +93 -0
- vcf_super_cli-0.5.0/docs/index.md +48 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/profiles.md +4 -0
- vcf_super_cli-0.5.0/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md +78 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/usage.md +29 -2
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/pyproject.toml +1 -1
- vcf_super_cli-0.5.0/tests/test_complete_cache.py +107 -0
- vcf_super_cli-0.5.0/tests/test_complete_dynamic.py +205 -0
- vcf_super_cli-0.5.0/tests/test_pyvmomi_find.py +342 -0
- vcf_super_cli-0.5.0/tests/test_resources.py +115 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/uv.lock +1 -1
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/_version.py +1 -1
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/builder.py +20 -3
- vcf_super_cli-0.5.0/vsc/gen/complete_cache.py +128 -0
- vcf_super_cli-0.5.0/vsc/gen/complete_dynamic.py +145 -0
- vcf_super_cli-0.5.0/vsc/gen/resources.py +135 -0
- vcf_super_cli-0.5.0/vsc/pyvmomi/find.py +187 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/inventory.py +60 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/assets/SKILL.md +11 -3
- vcf_super_cli-0.3.0/docs/design.md +0 -31
- vcf_super_cli-0.3.0/docs/index.md +0 -38
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/docs.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/e2e.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/lint.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/release.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/test.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.gitignore +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/AGENTS.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/LICENSE +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/install.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/writes.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/mkdocs.yml +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/README.md +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/conftest.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/test_read_smoke.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_builder.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_cli.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_complete.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_config.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_discover.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_errors.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_filter_pagination_cli.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_filters.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_paginate.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_params.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_preview.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_events.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_inventory.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_perf.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_tasks.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_render.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_session.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_skill.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_version.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_vmomi.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_write_errors.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/app.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/profiles.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/skill.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/schema.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/store.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/session.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/targets.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/vmomi.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/complete.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/discover.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/filters.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/model.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/paginate.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/params.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/preview.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/logging_config.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/errors.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/exit_codes.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/render.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/events.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/perf.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/runner.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/tasks.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/__init__.py +0 -0
- {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/export.py +0 -0
|
@@ -7,6 +7,54 @@ versions may include breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## v0.5.0 — 2026-06-08
|
|
11
|
+
|
|
12
|
+
Find VMs by attribute. Answers the everyday *"which VM has `10.20.3.41`?"* — a
|
|
13
|
+
reverse lookup the REST `vm list` filter can't do, because guest networking lives
|
|
14
|
+
only on the pyVmomi `guest.*` properties.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`vsc vsphere inventory find`** (#54) — locate VMs by guest/runtime attribute
|
|
19
|
+
without knowing the moid: `--ip` (exact or CIDR, IPv4/IPv6, across the primary and
|
|
20
|
+
every NIC address), `--name`, `--hostname`, `--guest-os` (case-insensitive
|
|
21
|
+
substring or glob), `--mac` (exact), and `--power-state`. Flags **AND** together; a
|
|
22
|
+
repeated flag **ORs** within its field; at least one match flag is required (it
|
|
23
|
+
refuses to dump the whole inventory). `--props` (repeatable) widens each hit's
|
|
24
|
+
output only — never a match criterion. One PropertyCollector round-trip over a
|
|
25
|
+
container view (always destroyed); a pure, pyVmomi-free matcher does the filtering.
|
|
26
|
+
Reads only — no `--apply` — and emits the same JSON / error envelope / exit codes
|
|
27
|
+
as the other pyVmomi fallback commands.
|
|
28
|
+
|
|
29
|
+
## v0.4.0 — 2026-06-08
|
|
30
|
+
|
|
31
|
+
Live resource-id completion. Pressing `<TAB>` on an id-typed argument can now
|
|
32
|
+
suggest **real ids** from the live inventory — strictly opt-in, cached, and
|
|
33
|
+
fail-soft. The agent-facing contract is unchanged: stable JSON, error envelope,
|
|
34
|
+
exit codes, dry-run-by-default writes, and `--help` stays fully offline.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Live resource-id completion** (#44), opt-in via `VSC_COMPLETE_DYNAMIC=1` and
|
|
39
|
+
off by default. When enabled, `<TAB>` on an id arg/option suggests live ids for
|
|
40
|
+
the resource type (VMs, hosts, clusters, datacenters, datastores, resource
|
|
41
|
+
pools), with each resource's name shown as completion help. Built on:
|
|
42
|
+
- a resource-type → list-op **registry** derived purely by introspecting the
|
|
43
|
+
SDK metadata (#45), no network;
|
|
44
|
+
- a **TTL cache** under the platform cache dir, keyed by
|
|
45
|
+
profile/backend/resource-type (default 60s, `VSC_COMPLETE_TTL` override) (#46);
|
|
46
|
+
- a **dynamic completer** whose fetch is time-bounded (`VSC_COMPLETE_TIMEOUT`,
|
|
47
|
+
default 2s) and blanket fail-soft — any error, missing auth, or timeout
|
|
48
|
+
yields no suggestions and is never cached (#47).
|
|
49
|
+
- Static/offline completion (enums, output formats, profiles, filter enums) is
|
|
50
|
+
unchanged and remains the default. `--help` and command execution never open a
|
|
51
|
+
connection for completion.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- Live completion is a human convenience only; agents should keep using `list`
|
|
56
|
+
to discover ids and must not depend on completion for correctness (#48).
|
|
57
|
+
|
|
10
58
|
## v0.3.0 — 2026-06-05
|
|
11
59
|
|
|
12
60
|
Ergonomics: friendlier filtering and paging, offline shell completion, and a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vcf-super-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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,8 @@ $ 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 |
|
|
78
79
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
79
80
|
|
|
80
81
|
## Install
|
|
@@ -42,7 +42,8 @@ $ 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 |
|
|
46
47
|
| NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
|
|
47
48
|
|
|
48
49
|
## 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
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
`vsc` builds its command tree by **statically introspecting the installed
|
|
4
|
+
`vcf-sdk` vAPI bindings**. Both `vmware-vcenter` and `vcf-nsx` are vAPI stub
|
|
5
|
+
libraries on the shared `vmware-vapi-runtime`:
|
|
6
|
+
|
|
7
|
+
- Each service is a `VapiInterface` subclass (`VM`, `Host`, `Cluster`,
|
|
8
|
+
`Datastore`, `Network`, …).
|
|
9
|
+
- Every operation carries `OperationRestMetadata` (`http_method`,
|
|
10
|
+
`path_variables`, `query_parameters`).
|
|
11
|
+
- Parameters are typed via `StructType` binding types.
|
|
12
|
+
|
|
13
|
+
A single generator walks those classes and emits one Typer command per
|
|
14
|
+
operation. `http_method == "GET"` ⇒ a read command; other verbs ⇒ writes
|
|
15
|
+
(dry-run by default + `--apply`). Because the metadata ships inside the SDK, the
|
|
16
|
+
tree builds **offline** — no server needed to render `--help`.
|
|
17
|
+
|
|
18
|
+
Top-level grouping:
|
|
19
|
+
|
|
20
|
+
- `vsc vsphere …` → `com.vmware.vcenter` (+ selected `com.vmware.esx`)
|
|
21
|
+
- `vsc nsx …` → `vcf.nsx.policy` (NSX Policy API)
|
|
22
|
+
|
|
23
|
+
The introspected intermediate representation is a list of `Operation` objects,
|
|
24
|
+
each carrying typed `Param`s (`vsc/gen/model.py`). Everything below is built from
|
|
25
|
+
that one model.
|
|
26
|
+
|
|
27
|
+
## Ergonomics (v0.3)
|
|
28
|
+
|
|
29
|
+
These build on the introspected `Param` model without changing the agent
|
|
30
|
+
contract.
|
|
31
|
+
|
|
32
|
+
### Offline shell completion
|
|
33
|
+
|
|
34
|
+
Completion values come entirely from the model and local config — **never a
|
|
35
|
+
network call**, so `<TAB>` stays fast and `--help` stays offline. Enum options
|
|
36
|
+
complete from their fixed choices, `--output` from the output formats, and
|
|
37
|
+
`--profile` from configured profile names (`vsc/gen/complete.py`). Completing a
|
|
38
|
+
live resource id would require a connection and is deferred to a later release.
|
|
39
|
+
|
|
40
|
+
### Per-field filter flags
|
|
41
|
+
|
|
42
|
+
A `list` operation takes a single `filter` parameter that is a struct
|
|
43
|
+
(`VM.FilterSpec` etc.). The generator flattens that struct into typed
|
|
44
|
+
`--<field>` options (repeatable for list/set fields; enum fields validate and
|
|
45
|
+
complete their choices). The raw `--filter '<json>'` blob remains as a base
|
|
46
|
+
layer that per-field flags merge **over** (`vsc/gen/filters.py`).
|
|
47
|
+
|
|
48
|
+
### Pagination
|
|
49
|
+
|
|
50
|
+
`list` commands gain `--all` (follow the NSX cursor across pages), `--max-items`
|
|
51
|
+
(cap the total), and `--limit` (client-side cap for non-paginated vSphere
|
|
52
|
+
lists). Without `--all`, a paginated list returns one page and surfaces its
|
|
53
|
+
`cursor` for manual paging; on non-cursor backends `--all` is a no-op and the
|
|
54
|
+
output stays a plain array (`vsc/gen/paginate.py`).
|
|
55
|
+
|
|
56
|
+
## pyVmomi fallback (v0.3)
|
|
57
|
+
|
|
58
|
+
A few inventory/performance areas are only reachable through the older pyVmomi
|
|
59
|
+
SOAP API. Those are **hand-written read-only commands** mounted under
|
|
60
|
+
`vsc vsphere` alongside the generated ones, sharing the same output contract:
|
|
61
|
+
|
|
62
|
+
- a separate connection path, `connect_vmomi()` via `SmartConnect`, reusing the
|
|
63
|
+
resolved vSphere credentials and honouring `--insecure` like the REST path
|
|
64
|
+
(`vsc/connect/vmomi.py`);
|
|
65
|
+
- `vmomi_jsonable()` collapses managed objects to their moref and data objects to
|
|
66
|
+
dicts so results render like any other;
|
|
67
|
+
- a shared `run_read()` runner maps pyVmomi faults onto the same error envelope
|
|
68
|
+
and exit codes (`vsc/pyvmomi/runner.py`).
|
|
69
|
+
|
|
70
|
+
Commands: `perf` (PerformanceManager counters), `events` / `tasks`
|
|
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`.
|
|
76
|
+
|
|
77
|
+
## Design specs
|
|
78
|
+
|
|
79
|
+
The authoritative, milestone-by-milestone designs live in the repository:
|
|
80
|
+
|
|
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)
|
|
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)
|
|
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)
|
|
85
|
+
|
|
86
|
+
## Contracts
|
|
87
|
+
|
|
88
|
+
- **Output:** JSON by default; `--output table` for humans.
|
|
89
|
+
- **Errors:** stable envelope `{ "error": { code, message, kind, details } }`.
|
|
90
|
+
- **Exit codes:** documented `IntEnum` (`0` ok, `1` generic, `2` usage, `3` auth,
|
|
91
|
+
`4` not-found, `5` connection, `6` config, `7` conflict, `8` unavailable).
|
|
92
|
+
- **Writes:** dry-run by default; `--apply` is the only gate and a dry-run opens
|
|
93
|
+
no connection.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# vcf-super-cli
|
|
2
|
+
|
|
3
|
+
A modern, agent-friendly CLI for **VMware Cloud Foundation 9** whose command tree
|
|
4
|
+
is **generated dynamically** from the official [`vcf-sdk`](https://pypi.org/project/vcf-sdk/)
|
|
5
|
+
vAPI bindings.
|
|
6
|
+
|
|
7
|
+
```console
|
|
8
|
+
$ vsc vsphere vm list --power-states POWERED_ON --profile prod
|
|
9
|
+
$ vsc nsx segments list --all --output table
|
|
10
|
+
$ vsc vsphere perf vm vm-42 --metric cpu.usage
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
!!! warning "Pre-1.0"
|
|
14
|
+
Reads and writes are both available. **Writes are dry-run by default** —
|
|
15
|
+
nothing changes without `--apply`. See [Writes](writes.md). While on `0.x`,
|
|
16
|
+
minor versions may include breaking changes.
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **Mirrors the real API** — commands come from the SDK's vAPI metadata, covering
|
|
21
|
+
vCenter and NSX from one generator.
|
|
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`).
|
|
26
|
+
- **pyVmomi fallback** — read-only `perf`, `events`, `tasks`, and `inventory`
|
|
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.
|
|
29
|
+
- **Safe by default** — writes are dry-run unless `--apply`; a dry-run never connects.
|
|
30
|
+
- **Agent-friendly** — JSON output, stable error envelope, documented exit codes,
|
|
31
|
+
bundled agent Skill.
|
|
32
|
+
|
|
33
|
+
See the [Design](design.md) for how dynamic generation works, and
|
|
34
|
+
[Commands](commands.md) for the full surface.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
uv tool install vcf-super-cli # or: pip install vcf-super-cli
|
|
40
|
+
vsc --install-completion # optional: offline shell completion
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
From source:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv sync
|
|
47
|
+
uv run vsc --help
|
|
48
|
+
```
|
|
@@ -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.
|
|
@@ -30,8 +30,35 @@ Completion is **fully offline** — it never opens a connection. It suggests:
|
|
|
30
30
|
- configured profile names (`--profile <TAB>`),
|
|
31
31
|
- and per-field `list` filter enum choices.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
### Live resource-id completion (opt-in)
|
|
34
|
+
|
|
35
|
+
Completing a real id (e.g. `<vm>` from the live inventory) does require a network
|
|
36
|
+
call, so it is **opt-in** and off by default:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
export VSC_COMPLETE_DYNAMIC=1
|
|
40
|
+
vsc vsphere vm get <TAB> # → vm-101 vm-102 … (real ids, names as help)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When enabled, pressing `<TAB>` on an id-typed argument or option suggests live
|
|
44
|
+
ids for that resource type (VMs, hosts, clusters, datacenters, datastores,
|
|
45
|
+
resource pools), showing each resource's name as completion help.
|
|
46
|
+
|
|
47
|
+
It is built to never get in your way:
|
|
48
|
+
|
|
49
|
+
- **Off by default.** Without `VSC_COMPLETE_DYNAMIC`, `<TAB>` stays fully offline
|
|
50
|
+
(the suggestions above are all you get).
|
|
51
|
+
- **Cached.** Results are cached per profile/backend/resource-type under the
|
|
52
|
+
platform cache dir with a short TTL (default 60s; override with
|
|
53
|
+
`VSC_COMPLETE_TTL=<seconds>`), so repeated presses don't re-hit the API.
|
|
54
|
+
- **Bounded and fail-soft.** The fetch is abandoned after a short timeout
|
|
55
|
+
(`VSC_COMPLETE_TIMEOUT`, default 2s); any error, missing auth, or timeout
|
|
56
|
+
yields no suggestions — `<TAB>` never hangs or prints a traceback.
|
|
57
|
+
- **`--help` is always offline.** Only the shell-completion subprocess ever
|
|
58
|
+
fetches; `--help` and command execution are unaffected.
|
|
59
|
+
|
|
60
|
+
This is a convenience only. The agent contract is unchanged: don't rely on
|
|
61
|
+
completion for correctness — list commands remain the source of truth for ids.
|
|
35
62
|
|
|
36
63
|
## Error envelope
|
|
37
64
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vcf-super-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.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"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""On-disk TTL cache for live completion candidates.
|
|
2
|
+
|
|
3
|
+
The cache keeps ``<TAB>`` fast and stops repeated completions from re-hitting the
|
|
4
|
+
API. It is fail-soft by contract: a missing, corrupt or unwritable cache is a
|
|
5
|
+
miss, never an exception — completion must never error or hang.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from vsc.gen.complete_cache import cache_dir, cache_ttl, get_or_fetch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def _tmp_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
20
|
+
monkeypatch.setenv("VSC_CACHE_DIR", str(tmp_path))
|
|
21
|
+
monkeypatch.delenv("VSC_COMPLETE_TTL", raising=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_hit_within_ttl_skips_fetch() -> None:
|
|
25
|
+
calls = 0
|
|
26
|
+
|
|
27
|
+
def fetch() -> list[tuple[str, str]]:
|
|
28
|
+
nonlocal calls
|
|
29
|
+
calls += 1
|
|
30
|
+
return [("vm-1", "web"), ("vm-2", "db")]
|
|
31
|
+
|
|
32
|
+
first = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1000.0)
|
|
33
|
+
second = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1059.0)
|
|
34
|
+
assert first == [("vm-1", "web"), ("vm-2", "db")]
|
|
35
|
+
assert second == first
|
|
36
|
+
assert calls == 1 # served from cache the second time
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_expired_entry_refetches() -> None:
|
|
40
|
+
calls = 0
|
|
41
|
+
|
|
42
|
+
def fetch() -> list[tuple[str, str]]:
|
|
43
|
+
nonlocal calls
|
|
44
|
+
calls += 1
|
|
45
|
+
return [(f"vm-{calls}", "x")]
|
|
46
|
+
|
|
47
|
+
get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1000.0)
|
|
48
|
+
again = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1061.0)
|
|
49
|
+
assert calls == 2
|
|
50
|
+
assert again == [("vm-2", "x")]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_corrupt_file_is_a_miss() -> None:
|
|
54
|
+
# Pre-create a garbage cache file at the key's path; get_or_fetch must treat
|
|
55
|
+
# it as a miss and never raise.
|
|
56
|
+
get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "a")], now=0.0)
|
|
57
|
+
files = list(cache_dir().glob("*.json"))
|
|
58
|
+
assert files
|
|
59
|
+
files[0].write_text("}{ not json", encoding="utf-8")
|
|
60
|
+
out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("b", "b")], now=1.0)
|
|
61
|
+
assert out == [("b", "b")]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_cache_dir_created_lazily() -> None:
|
|
65
|
+
assert not cache_dir().exists()
|
|
66
|
+
get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "a")], now=0.0)
|
|
67
|
+
assert cache_dir().exists()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_write_failure_is_failsoft(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
71
|
+
def boom(*_a: object, **_k: object) -> None:
|
|
72
|
+
raise OSError("disk full")
|
|
73
|
+
|
|
74
|
+
monkeypatch.setattr("vsc.gen.complete_cache.os.replace", boom)
|
|
75
|
+
out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "n")], now=0.0)
|
|
76
|
+
assert out == [("a", "n")] # fetched value still returned despite write failure
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_items_round_trip_as_tuples() -> None:
|
|
80
|
+
get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("vm-1", "web")], now=0.0)
|
|
81
|
+
out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("zzz", "zzz")], now=1.0)
|
|
82
|
+
assert out == [("vm-1", "web")]
|
|
83
|
+
assert all(isinstance(item, tuple) and len(item) == 2 for item in out)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_malformed_entries_in_payload_are_dropped() -> None:
|
|
87
|
+
get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "n")], now=0.0)
|
|
88
|
+
path = next(iter(cache_dir().glob("*.json")))
|
|
89
|
+
path.write_text(
|
|
90
|
+
json.dumps({"ts": 0.0, "items": [["ok", "name"], "bad", ["x"], [1, 2, 3]]}),
|
|
91
|
+
encoding="utf-8",
|
|
92
|
+
)
|
|
93
|
+
out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("z", "z")], now=1.0)
|
|
94
|
+
assert out == [("ok", "name")]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_cache_ttl_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
98
|
+
assert cache_ttl() == 60.0
|
|
99
|
+
monkeypatch.setenv("VSC_COMPLETE_TTL", "5")
|
|
100
|
+
assert cache_ttl() == 5.0
|
|
101
|
+
monkeypatch.setenv("VSC_COMPLETE_TTL", "not-a-number")
|
|
102
|
+
assert cache_ttl() == 60.0 # invalid -> default, never raises
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_cache_dir_under_override() -> None:
|
|
106
|
+
assert cache_dir().parent == Path(__import__("os").environ["VSC_CACHE_DIR"])
|
|
107
|
+
assert cache_dir().name == "completion"
|