vsphere-triage 0.1.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 (47) hide show
  1. vsphere_triage-0.1.0/.github/workflows/ci.yml +59 -0
  2. vsphere_triage-0.1.0/.github/workflows/release.yml +234 -0
  3. vsphere_triage-0.1.0/.gitignore +8 -0
  4. vsphere_triage-0.1.0/AGENTS.md +56 -0
  5. vsphere_triage-0.1.0/LICENSE +21 -0
  6. vsphere_triage-0.1.0/PKG-INFO +364 -0
  7. vsphere_triage-0.1.0/README.md +337 -0
  8. vsphere_triage-0.1.0/docs/design.md +588 -0
  9. vsphere_triage-0.1.0/docs/orchestrator-integration.md +58 -0
  10. vsphere_triage-0.1.0/pyproject.toml +79 -0
  11. vsphere_triage-0.1.0/skills/vm-incident/SKILL.md +169 -0
  12. vsphere_triage-0.1.0/skills/vm-incident/core/causes.md +84 -0
  13. vsphere_triage-0.1.0/skills/vm-incident/core/correlation.md +71 -0
  14. vsphere_triage-0.1.0/skills/vm-incident/core/ladder.md +112 -0
  15. vsphere_triage-0.1.0/skills/vm-incident/core/verdict.md +74 -0
  16. vsphere_triage-0.1.0/skills/vm-incident/evals/evals.json +102 -0
  17. vsphere_triage-0.1.0/skills/vm-incident/examples/cpu-overcommit.md +69 -0
  18. vsphere_triage-0.1.0/skills/vm-incident/examples/storage-boot-storm.md +74 -0
  19. vsphere_triage-0.1.0/skills/vm-incident/glossary.md +28 -0
  20. vsphere_triage-0.1.0/tests/integration/conftest.py +192 -0
  21. vsphere_triage-0.1.0/tests/integration/scenario.py +800 -0
  22. vsphere_triage-0.1.0/tests/integration/test_fleet.py +552 -0
  23. vsphere_triage-0.1.0/tests/integration/test_signals.py +360 -0
  24. vsphere_triage-0.1.0/tests/test_logic.py +1379 -0
  25. vsphere_triage-0.1.0/vsphere_triage/__init__.py +1 -0
  26. vsphere_triage-0.1.0/vsphere_triage/__main__.py +5 -0
  27. vsphere_triage-0.1.0/vsphere_triage/ages.py +183 -0
  28. vsphere_triage-0.1.0/vsphere_triage/checks.py +337 -0
  29. vsphere_triage-0.1.0/vsphere_triage/cli.py +67 -0
  30. vsphere_triage-0.1.0/vsphere_triage/config.py +359 -0
  31. vsphere_triage-0.1.0/vsphere_triage/console.py +210 -0
  32. vsphere_triage-0.1.0/vsphere_triage/contention.py +408 -0
  33. vsphere_triage-0.1.0/vsphere_triage/diagnose.py +1013 -0
  34. vsphere_triage-0.1.0/vsphere_triage/formatting.py +58 -0
  35. vsphere_triage-0.1.0/vsphere_triage/guestexec.py +112 -0
  36. vsphere_triage-0.1.0/vsphere_triage/hardware.py +219 -0
  37. vsphere_triage-0.1.0/vsphere_triage/inventory.py +1184 -0
  38. vsphere_triage-0.1.0/vsphere_triage/readonly.py +111 -0
  39. vsphere_triage-0.1.0/vsphere_triage/resources.py +172 -0
  40. vsphere_triage-0.1.0/vsphere_triage/scan.py +669 -0
  41. vsphere_triage-0.1.0/vsphere_triage/server.py +1333 -0
  42. vsphere_triage-0.1.0/vsphere_triage/snapshots.py +174 -0
  43. vsphere_triage-0.1.0/vsphere_triage/storagehealth.py +180 -0
  44. vsphere_triage-0.1.0/vsphere_triage/timecheck.py +163 -0
  45. vsphere_triage-0.1.0/vsphere_triage/vcenter.py +920 -0
  46. vsphere_triage-0.1.0/vsphere_triage/vmlog.py +371 -0
  47. vsphere_triage-0.1.0/vsphere_triage/vrops.py +240 -0
@@ -0,0 +1,59 @@
1
+ name: CI
2
+
3
+ # The per-commit gate: lint, format and the unit suite, on Linux only. The
4
+ # vcsim integration suite pulls two Docker images and builds two simulated
5
+ # vCenters, so it is NOT run on every push — dispatch it by hand when you need
6
+ # it (Actions -> CI -> Run workflow -> integration = true), or before a release.
7
+ # release.yml reuses this file via workflow_call.
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+ workflow_call:
13
+ workflow_dispatch:
14
+ inputs:
15
+ integration:
16
+ description: "Also run the vcsim integration suite (Docker)"
17
+ type: boolean
18
+ default: false
19
+
20
+ # One cheap runner per ref. Superseded pull-request runs are cancelled; pushes
21
+ # to main are left to finish.
22
+ concurrency:
23
+ group: ci-${{ github.workflow }}-${{ github.ref }}
24
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ check:
31
+ name: Lint & unit tests
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 10
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: astral-sh/setup-uv@v5
37
+ with:
38
+ enable-cache: true
39
+ - name: Lint
40
+ run: uv run --extra dev ruff check .
41
+ - name: Format
42
+ run: uv run --extra dev ruff format --check .
43
+ - name: Unit tests
44
+ run: uv run --extra dev python -m pytest -m "not integration" -q
45
+
46
+ integration:
47
+ name: vcsim integration (Docker)
48
+ # Expensive: pulls vmware/vcsim + vmware/govc and builds two simulated
49
+ # vCenters. Manual only, so it never runs on an ordinary push.
50
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.integration }}
51
+ runs-on: ubuntu-latest
52
+ timeout-minutes: 25
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ - uses: astral-sh/setup-uv@v5
56
+ with:
57
+ enable-cache: true
58
+ - name: Full suite (unit + vcsim integration)
59
+ run: uv run --extra dev python -m pytest -q
@@ -0,0 +1,234 @@
1
+ name: Release
2
+
3
+ # Bump the version in pyproject.toml, merge to main, and the release runs
4
+ # itself: a push to main whose version has no vX.Y.Z tag yet builds the sdist +
5
+ # wheel, verifies the wheel installs and imports on Linux, publishes to PyPI,
6
+ # then tags the commit vX.Y.Z and makes the GitHub release. A merge that does
7
+ # not change the version stops at the gate. Tagging by hand still works:
8
+ #
9
+ # git tag v0.2.0 && git push origin v0.2.0
10
+ #
11
+ # Publishing uses PyPI Trusted Publishing (OIDC) — there is no token in the
12
+ # repo. Configure the publisher once at pypi.org/manage/account/publishing
13
+ # (project: vsphere-triage, workflow: release.yml, environment: pypi), and the
14
+ # same for test.pypi.org if you use the testpypi dispatch.
15
+ #
16
+ # Linux only, on purpose: this is a pure-Python package, so one platform
17
+ # verifies the artifact and keeps the Actions bill small.
18
+ on:
19
+ push:
20
+ branches: [main]
21
+ tags: ["v*"]
22
+ workflow_dispatch:
23
+ inputs:
24
+ repository:
25
+ description: "Where to publish from the current branch"
26
+ type: choice
27
+ options: [testpypi, pypi]
28
+ default: testpypi
29
+
30
+ permissions:
31
+ contents: read
32
+
33
+ # Two merges in quick succession release one after the other, never at once.
34
+ concurrency:
35
+ group: release
36
+ cancel-in-progress: false
37
+
38
+ jobs:
39
+ gate:
40
+ name: Anything to release?
41
+ runs-on: ubuntu-latest
42
+ timeout-minutes: 5
43
+ outputs:
44
+ release: ${{ steps.gate.outputs.release }}
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - name: Is pyproject.toml's version tagged yet?
48
+ id: gate
49
+ shell: bash
50
+ run: |
51
+ set -euo pipefail
52
+ # A tag push or a manual dispatch is a release by definition.
53
+ if [ "$GITHUB_EVENT_NAME" != push ] || [ "$GITHUB_REF_TYPE" = tag ]; then
54
+ echo "release=true" >> "$GITHUB_OUTPUT"; exit 0
55
+ fi
56
+ version=$(python3 -c "import pathlib,tomllib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
57
+ if git ls-remote --exit-code origin "refs/tags/v$version" >/dev/null; then
58
+ echo "v$version is already released and this merge does not bump the version: nothing to do."
59
+ echo "release=false" >> "$GITHUB_OUTPUT"
60
+ else
61
+ echo "v$version is not tagged yet: releasing it."
62
+ echo "release=true" >> "$GITHUB_OUTPUT"
63
+ fi
64
+
65
+ build:
66
+ name: Build
67
+ needs: gate
68
+ if: needs.gate.outputs.release == 'true'
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 10
71
+ outputs:
72
+ version: ${{ steps.version.outputs.version }}
73
+ prerelease: ${{ steps.version.outputs.prerelease }}
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ - uses: actions/setup-python@v5
77
+ with:
78
+ python-version: "3.12"
79
+ - name: Read the version, and check a tag agrees with it
80
+ id: version
81
+ shell: bash
82
+ run: |
83
+ set -euo pipefail
84
+ version=$(python -c "import pathlib,tomllib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
85
+ echo "version=$version" >> "$GITHUB_OUTPUT"
86
+ # A tag vX.Y.Z that builds a different version uploads the wrong thing
87
+ # under the right name, and PyPI will not let you take it back.
88
+ if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
89
+ tagged="${GITHUB_REF_NAME#v}"
90
+ if [ "$tagged" != "$version" ]; then
91
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $version"; exit 1
92
+ fi
93
+ fi
94
+ # A tag for this version that already exists must be on this commit —
95
+ # checked here, before the irreversible publish, not only after it.
96
+ at=$(git ls-remote origin "refs/tags/v$version" "refs/tags/v$version^{}" | awk 'END {print $1}')
97
+ if [ -n "$at" ] && [ "$at" != "$GITHUB_SHA" ]; then
98
+ echo "::error::v$version is already on $at, not the commit this run would publish ($GITHUB_SHA)"; exit 1
99
+ fi
100
+ # PEP 440 prereleases: 1.2.0rc1, no separator.
101
+ if printf '%s' "$version" | grep -Eq '(a|b|rc|dev)[0-9]*$'; then
102
+ echo "prerelease=true" >> "$GITHUB_OUTPUT"
103
+ else
104
+ echo "prerelease=false" >> "$GITHUB_OUTPUT"
105
+ fi
106
+ - name: Build the sdist and the wheel
107
+ run: |
108
+ python -m pip install --upgrade pip build twine
109
+ python -m build
110
+ twine check --strict dist/*
111
+ - run: ls -l dist/
112
+ - uses: actions/upload-artifact@v4
113
+ with:
114
+ name: distribution
115
+ path: dist/
116
+ if-no-files-found: error
117
+
118
+ verify:
119
+ # Verify the built artifact, not the checkout: install the wheel into a
120
+ # clean venv, import the package, construct the server, and run the console
121
+ # script — so a packaging mistake (a missing module, a broken entry point)
122
+ # is caught before publish, not after.
123
+ name: Verify the wheel
124
+ needs: build
125
+ runs-on: ubuntu-latest
126
+ timeout-minutes: 10
127
+ steps:
128
+ - uses: actions/setup-python@v5
129
+ with:
130
+ python-version: "3.12"
131
+ - uses: actions/download-artifact@v4
132
+ with:
133
+ name: distribution
134
+ path: dist
135
+ - name: Install the built wheel and smoke-test it
136
+ shell: bash
137
+ run: |
138
+ set -euo pipefail
139
+ python -m venv /tmp/venv
140
+ /tmp/venv/bin/pip install --upgrade pip
141
+ /tmp/venv/bin/pip install "$(ls dist/*.whl)"
142
+ /tmp/venv/bin/python - <<'CHECK'
143
+ import vsphere_triage
144
+ from vsphere_triage.config import Settings
145
+ from vsphere_triage.server import build_server
146
+ located = vsphere_triage.__file__.replace("\\", "/")
147
+ assert "site-packages" in located, f"tested the checkout, not the wheel: {located}"
148
+ settings = Settings.from_environment({
149
+ "VSPHERE_VCENTERS": '[{"name": "vc", "host": "h"}]',
150
+ "VSPHERE_USERNAME": "u",
151
+ "VSPHERE_PASSWORD": "p",
152
+ })
153
+ build_server(settings) # the whole tool surface wires up
154
+ print("imported from:", located)
155
+ CHECK
156
+ /tmp/venv/bin/vsphere-triage --help > /dev/null
157
+ echo "console script runs"
158
+
159
+ publish:
160
+ name: Publish to ${{ (github.event_name == 'push' && 'pypi') || inputs.repository }}
161
+ needs: [build, verify]
162
+ runs-on: ubuntu-latest
163
+ timeout-minutes: 10
164
+ # Trusted publishing: PyPI verifies this workflow's OIDC identity, so there
165
+ # is no long-lived token in the repo. A manual approval gate, if wanted, is
166
+ # a property of the environment (Settings -> Environments -> pypi ->
167
+ # Required reviewers), not of this file, so it cannot be removed by editing
168
+ # a branch.
169
+ environment:
170
+ name: ${{ (github.event_name == 'push' && 'pypi') || inputs.repository }}
171
+ url: ${{ ((github.event_name == 'push' || inputs.repository == 'pypi') && 'https://pypi.org/p/vsphere-triage') || 'https://test.pypi.org/p/vsphere-triage' }}
172
+ permissions:
173
+ id-token: write
174
+ steps:
175
+ - uses: actions/download-artifact@v4
176
+ with:
177
+ name: distribution
178
+ path: dist
179
+ - uses: pypa/gh-action-pypi-publish@release/v1
180
+ with:
181
+ repository-url: ${{ ((github.event_name == 'push' || inputs.repository == 'pypi') && 'https://upload.pypi.org/legacy/') || 'https://test.pypi.org/legacy/' }}
182
+ print-hash: true
183
+ # So a run that uploaded and then failed later can be re-run: PyPI
184
+ # never replaces a file it already has, whatever this is set to.
185
+ skip-existing: true
186
+
187
+ release:
188
+ # Last, and only once PyPI has accepted the upload. The runner's token
189
+ # writes the tag, so a release need not be tagged up front.
190
+ name: Tag and release
191
+ needs: [build, publish]
192
+ if: github.event_name == 'push' || inputs.repository == 'pypi'
193
+ runs-on: ubuntu-latest
194
+ timeout-minutes: 10
195
+ permissions:
196
+ contents: write
197
+ steps:
198
+ - uses: actions/checkout@v4
199
+ - uses: actions/download-artifact@v4
200
+ with:
201
+ name: distribution
202
+ path: dist
203
+ - name: Make sure the published commit is tagged
204
+ shell: bash
205
+ run: |
206
+ set -euo pipefail
207
+ tag="v${{ needs.build.outputs.version }}"
208
+ at=$(git ls-remote origin "refs/tags/$tag" "refs/tags/$tag^{}" | awk 'END {print $1}')
209
+ if [ -n "$at" ]; then
210
+ if [ "$at" != "$GITHUB_SHA" ]; then
211
+ echo "::error::$tag is already on $at, not the commit this run published ($GITHUB_SHA)"; exit 1
212
+ fi
213
+ echo "$tag already exists here (tag-push path)"
214
+ else
215
+ git config user.name "github-actions[bot]"
216
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
217
+ git tag -a "$tag" -m "vsphere-triage ${{ needs.build.outputs.version }}"
218
+ git push origin "$tag"
219
+ fi
220
+ echo "TAG=$tag" >> "$GITHUB_ENV"
221
+ - name: Create the GitHub release
222
+ env:
223
+ GH_TOKEN: ${{ github.token }}
224
+ run: |
225
+ set -euo pipefail
226
+ if gh release view "$TAG" >/dev/null 2>&1; then
227
+ gh release upload "$TAG" dist/* --clobber
228
+ else
229
+ gh release create "$TAG" dist/* \
230
+ --title "$TAG" \
231
+ --generate-notes \
232
+ --verify-tag \
233
+ ${{ needs.build.outputs.prerelease == 'true' && '--prerelease' || '' }}
234
+ fi
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ *.egg-info/
5
+ .venv/
6
+ uv.lock
7
+ dist/
8
+ build/
@@ -0,0 +1,56 @@
1
+ # CI policy for coding agents
2
+
3
+ CI cost matters here. The workflows are deliberately **Linux-only and
4
+ single-job**, because this is a pure-Python package that needs no
5
+ cross-platform matrix. Keep them that way unless a real portability bug forces
6
+ otherwise.
7
+
8
+ ## The per-commit gate
9
+
10
+ `.github/workflows/ci.yml` runs on every push and pull request: one job that
11
+ lints (`ruff check`), checks formatting (`ruff format --check`), and runs the
12
+ **unit** suite (`pytest -m "not integration"`). That is the whole required
13
+ gate. Do not add an OS matrix, extra Python versions, or more jobs to it to
14
+ chase a single failure — a change that makes CI slower or costlier needs a
15
+ reason beyond convenience.
16
+
17
+ ## The integration suite is not free, and not per-commit
18
+
19
+ `tests/integration/` builds **two simulated vCenters** with `vmware/vcsim` and
20
+ `vmware/govc` (two image pulls, ~90 s of work). It is marked with the
21
+ `integration` pytest marker and is **never** on the per-commit path. Run it:
22
+
23
+ - locally: `uv run --extra dev python -m pytest` (needs a running Docker), or
24
+ - on CI by hand: Actions → CI → Run workflow → `integration = true`.
25
+
26
+ Dispatch it before a release, or when you change anything that touches the
27
+ vCenter session layer, the read-only guard, or the scenario fixtures. Do not
28
+ wire it into `ci.yml`'s automatic triggers.
29
+
30
+ ## Local iteration
31
+
32
+ - `uv run --extra dev python -m pytest -m "not integration" -q` — the fast gate
33
+ (no Docker, ~1 s).
34
+ - `uv run --extra dev python -m pytest -q` — everything, Docker required.
35
+ - `pytest --lf` / `pytest --ff` — re-run last failures first while iterating;
36
+ don't delete `.pytest_cache` mid-investigation.
37
+
38
+ ## Releasing
39
+
40
+ `.github/workflows/release.yml` publishes to PyPI via **Trusted Publishing**
41
+ (OIDC) — there is **no token** in the repo, and none should be added. Release
42
+ is a version bump: raise `version` in `pyproject.toml`, merge to `main`, and the
43
+ workflow builds, verifies the wheel, publishes, tags, and makes the GitHub
44
+ release. A merge that does not change the version stops at the gate. Never
45
+ commit a PyPI token, and never weaken the version/tag consistency checks in
46
+ `build` — they are the guard against publishing the wrong artifact under a
47
+ version PyPI will not let you reclaim.
48
+
49
+ ## The read-only guarantee
50
+
51
+ Every tool reads only, with one marked exception (`run_in_guest`). A test sweeps
52
+ every read-only tool and asserts the vSphere methods sent are a subset of
53
+ `readonly.ALLOWED_METHODS`; another asserts guest exec never dirties that
54
+ surface. If you add a tool or a vSphere call, keep both tests passing — do not
55
+ add a method to the allowlist to silence the sweep without understanding why it
56
+ is being sent.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Heknon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.