From bab4651bc1b8876a64336d7bcf4d1004e5c92a02 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:35:24 +0100 Subject: [PATCH 01/12] From 10ad0c3ef9d1d1fb60999fd616e5192d5a245145 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:01:38 +0100 Subject: [PATCH 02/12] docs: add CLAUDE.md with architecture and development guidance Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3c01eed1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`pvecontrol` is a Python CLI tool for managing Proxmox VE clusters. It wraps the [proxmoxer](https://pypi.org/project/proxmoxer/) library to provide higher-level operations not available in the Proxmox web UI (bulk VM listing, node evacuation/migration, sanity checks). + +## Development Setup + +```shell +# Activate the virtual environment +source .env/bin/activate + +# Run the tool +pvecontrol --help +``` + +To recreate the environment from scratch: + +```shell +python3 -m venv .env +.env/bin/pip install -r requirements.txt -r requirements-dev.txt -e . +``` + +## Commands + +```shell +# Run all tests +pytest src/ + +# Run a single test file +pytest src/tests/test_cluster.py + +# Lint +pylint src/pvecontrol/ + +# Format (line length 120) +black src/ +``` + +## Architecture + +The codebase follows a clean separation between CLI, business logic (actions), and domain models. + +**Entry point**: `src/pvecontrol/__init__.py` — defines the Click group `pvecontrol`, wires all subcommands, and exports `main()`. + +**CLI decorators**: `src/pvecontrol/cli.py` — reusable Click decorators (`with_table_options`, `task_related_command`, `migration_related_command`, `ResourceGroup`) shared across action modules. + +**Actions** (`src/pvecontrol/actions/`): One module per resource type (`cluster`, `node`, `vm`, `storage`, `task`). Each module defines Click commands and calls into models. Actions instantiate `PVECluster.create_from_config(cluster_name)` to get a connected cluster object. + +**Models** (`src/pvecontrol/models/`): Domain objects wrapping the Proxmox API: +- `PVECluster` — top-level object; holds `nodes`, `storages`, lazy-loaded `tasks`, `ha`, `backups`, `backup_jobs`; created via `create_from_config()` +- `PVENode` — holds a list of `PVEVm` instances; computes `allocatedcpu` / `allocatedmem` +- `PVEVm`, `PVEStorage`, `PVEVolume`, `PVETask`, `PVEBackupJob` — thin wrappers around API data + +**Configuration**: `src/pvecontrol/config.py` uses [confuse](https://confuse.readthedocs.io/) to load `~/.config/pvecontrol/config.yaml`. Supports `$()` shell command substitution in `user`, `password`, `token_name`, `token_value`, and `proxy_certificate` fields. + +**Output**: `src/pvecontrol/utils.py` — `print_output()` / `render_output()` render data via `prettytable` in text/json/csv/yaml/md formats. Memory/disk keys in `NATURALSIZE_KEYS` are automatically humanized. + +**Sanity checks** (`src/pvecontrol/sanitycheck/`): +- `checks.py` — abstract base `Check` class with `CheckCode` (OK/WARN/INFO/CRIT) and `CheckType` enums +- `tests/` — concrete check implementations: `Nodes`, `HaGroups`, `HaVms`, `VmsStartOnBoot`, `VmBackups`, `DiskUnused` +- `sanitychecks.py` — `SanityCheck` orchestrates running checks and displaying results; exits with code 1 on any CRIT + +**Tests** (`src/tests/`): Use `unittest` + `responses` for HTTP mocking. `PVEControlTestcase` in `testcase.py` provides a pre-wired cluster fixture with fake nodes, VMs, backups. Test fixtures live in `src/tests/fixtures/api.py`. + +## Conventions + +- Commits must follow [Angular Conventional Commits](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format) — releases are automated via `python-semantic-release` +- Allowed commit tags: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `style`, `refactor`, `test` +- All changes must go through a PR with review; `main` is protected +- Line length: 120 (black) / 150 (pylint) From 0fd92e0aec345c8dda0b45d07e848417a622ffbe Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:51:23 +0100 Subject: [PATCH 03/12] fix(sanitychecks): Fix pytest errors This fix pytest errors because of updates into confuse --- src/pvecontrol/sanitycheck/sanitychecks.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pvecontrol/sanitycheck/sanitychecks.py b/src/pvecontrol/sanitycheck/sanitychecks.py index 6775d476..f2bb78b1 100644 --- a/src/pvecontrol/sanitycheck/sanitychecks.py +++ b/src/pvecontrol/sanitycheck/sanitychecks.py @@ -1,4 +1,16 @@ +<<<<<<< ours +||||||| ancestor +from pvecontrol.models.cluster import PVECluster +======= +from __future__ import annotations + +from typing import TYPE_CHECKING + +>>>>>>> theirs from pvecontrol.sanitycheck.checks import CheckCode + +if TYPE_CHECKING: + from pvecontrol.models.cluster import PVECluster from pvecontrol.sanitycheck.tests import DEFAULT_CHECKS, DEFAULT_CHECK_IDS from pvecontrol.models.cluster import PVECluster From 083aa943482186aa6fc46616b3b2189d2f94df20 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:56:50 +0100 Subject: [PATCH 04/12] chore(lint): Fix linting issue --- src/pvecontrol/sanitycheck/sanitychecks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pvecontrol/sanitycheck/sanitychecks.py b/src/pvecontrol/sanitycheck/sanitychecks.py index f2bb78b1..0e405334 100644 --- a/src/pvecontrol/sanitycheck/sanitychecks.py +++ b/src/pvecontrol/sanitycheck/sanitychecks.py @@ -8,10 +8,10 @@ >>>>>>> theirs from pvecontrol.sanitycheck.checks import CheckCode +from pvecontrol.sanitycheck.tests import DEFAULT_CHECKS, DEFAULT_CHECK_IDS if TYPE_CHECKING: from pvecontrol.models.cluster import PVECluster -from pvecontrol.sanitycheck.tests import DEFAULT_CHECKS, DEFAULT_CHECK_IDS from pvecontrol.models.cluster import PVECluster From fe41bb61723646f54aa1e6e8f39cbeac2c212a6f Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Fri, 13 Mar 2026 14:52:40 +0100 Subject: [PATCH 05/12] fix(tests): remove unneded future --- src/pvecontrol/sanitycheck/sanitychecks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pvecontrol/sanitycheck/sanitychecks.py b/src/pvecontrol/sanitycheck/sanitychecks.py index 0e405334..90adad60 100644 --- a/src/pvecontrol/sanitycheck/sanitychecks.py +++ b/src/pvecontrol/sanitycheck/sanitychecks.py @@ -9,9 +9,7 @@ >>>>>>> theirs from pvecontrol.sanitycheck.checks import CheckCode from pvecontrol.sanitycheck.tests import DEFAULT_CHECKS, DEFAULT_CHECK_IDS - -if TYPE_CHECKING: - from pvecontrol.models.cluster import PVECluster +from pvecontrol.models.cluster import PVECluster from pvecontrol.models.cluster import PVECluster From 94d590bb08c834ff1c3ff2486a8e456c2bf87c6c Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Tue, 10 Mar 2026 15:06:04 +0100 Subject: [PATCH 06/12] fix(cluster.py): Ha rules HA groups have been replaced by HA rules in proxmox ve version 9.1+ --- src/pvecontrol/actions/cluster.py | 2 ++ src/pvecontrol/models/cluster.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pvecontrol/actions/cluster.py b/src/pvecontrol/actions/cluster.py index 7b9e91df..c09eddfd 100644 --- a/src/pvecontrol/actions/cluster.py +++ b/src/pvecontrol/actions/cluster.py @@ -16,6 +16,7 @@ def status(ctx): """Show cluster status""" proxmox = PVECluster.create_from_config(ctx.obj["args"].cluster) + cluster_version = proxmox.version cluster_status = "healthy" if proxmox.is_healthy else "not healthy" templates = sum(len(node.templates) for node in proxmox.nodes) @@ -44,6 +45,7 @@ def _get_disk_output(): if ctx.obj["args"].output == OutputFormats.TEXT: print(f"""\n\ + Version: {cluster_version['version']} Status: {cluster_status} VMs: {vms - templates} Templates: {templates} diff --git a/src/pvecontrol/models/cluster.py b/src/pvecontrol/models/cluster.py index e0b0e485..6357fcd1 100644 --- a/src/pvecontrol/models/cluster.py +++ b/src/pvecontrol/models/cluster.py @@ -32,6 +32,7 @@ def __init__(self, name, host, config, timeout, verify_ssl=False, **auth): self._initstatus() def _initstatus(self): + self.version = self.api.version.get() self.status = self.api.cluster.status.get() self.resources = self.api.cluster.resources.get() @@ -74,8 +75,13 @@ def ha(self): if self._ha is not None: return self._ha + # use rules instead of ha in newer versions + if float(self.version['release']) >= 9.1: + _ha_groups= self.api.cluster.ha.rules.get() + else: + _ha_groups= self.api.cluster.ha.groups.get() self._ha = { - "groups": self.api.cluster.ha.groups.get(), + "groups": _ha_groups, "manager_status": self.api.cluster.ha.status.manager_status.get(), "resources": self.api.cluster.ha.resources.get(), } From d3f67ff7480a7b916346d69131b0d3cb59ce89e9 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Tue, 10 Mar 2026 14:46:55 +0100 Subject: [PATCH 07/12] fix(vm.py): Add Unknown status Add unknow vm status --- src/pvecontrol/models/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pvecontrol/models/vm.py b/src/pvecontrol/models/vm.py index 27e4cf20..37f90f04 100644 --- a/src/pvecontrol/models/vm.py +++ b/src/pvecontrol/models/vm.py @@ -10,6 +10,7 @@ class VmStatus(Enum): SUSPENDED = 3 POSTMIGRATE = 4 PRELAUNCH = 5 + UNKNOWN = 6 class PVEVm: From 6ea6d4398c396e6ed6135ca1d1689c449bd2f38f Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Tue, 10 Mar 2026 15:58:35 +0100 Subject: [PATCH 08/12] chore(black): Update linting --- src/pvecontrol/models/cluster.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pvecontrol/models/cluster.py b/src/pvecontrol/models/cluster.py index 6357fcd1..7c3cba38 100644 --- a/src/pvecontrol/models/cluster.py +++ b/src/pvecontrol/models/cluster.py @@ -76,10 +76,10 @@ def ha(self): return self._ha # use rules instead of ha in newer versions - if float(self.version['release']) >= 9.1: - _ha_groups= self.api.cluster.ha.rules.get() + if float(self.version["release"]) >= 9.1: + _ha_groups = self.api.cluster.ha.rules.get() else: - _ha_groups= self.api.cluster.ha.groups.get() + _ha_groups = self.api.cluster.ha.groups.get() self._ha = { "groups": _ha_groups, "manager_status": self.api.cluster.ha.status.manager_status.get(), From 31de9bada8c7b53e80b219e0924d3541256c80e4 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Wed, 11 Mar 2026 15:20:54 +0100 Subject: [PATCH 09/12] fix(tests): Add version api mockup --- src/tests/fixtures/api.py | 1 + src/tests/testcase.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/tests/fixtures/api.py b/src/tests/fixtures/api.py index 23f14152..2f8c894f 100644 --- a/src/tests/fixtures/api.py +++ b/src/tests/fixtures/api.py @@ -78,6 +78,7 @@ def wrapper(path, data=None, **kwargs): def generate_routes(nodes, vms, backup_jobs, storage_resources=None, storage_contents=None): storage_resources = storage_resources or [] routes = { + "/api2/json/version": {"version": "9.1.4", "release": "9.1", "repoid": "5ac30304265fbd8e"}, "/api2/json/cluster/status": get_status(nodes), "/api2/json/cluster/resources": get_resources(nodes, vms, storage_resources), "/api2/json/nodes": get_node_resources(nodes), diff --git a/src/tests/testcase.py b/src/tests/testcase.py index 77f575d0..2b881dd3 100644 --- a/src/tests/testcase.py +++ b/src/tests/testcase.py @@ -49,6 +49,7 @@ def setUp(self): self.nodes, self.vms, self.backup_jobs, self.storage_resources, self.storages_contents ) + self.responses_get("/api2/json/version") self.responses_get("/api2/json/cluster/status") self.responses_get("/api2/json/cluster/resources") From 506fe05ea724d7e4edbc8a0858c54f33f06a33ec Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Fri, 13 Mar 2026 15:45:42 +0100 Subject: [PATCH 10/12] fix(rebase): bad rebase --- src/pvecontrol/sanitycheck/sanitychecks.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pvecontrol/sanitycheck/sanitychecks.py b/src/pvecontrol/sanitycheck/sanitychecks.py index 90adad60..6775d476 100644 --- a/src/pvecontrol/sanitycheck/sanitychecks.py +++ b/src/pvecontrol/sanitycheck/sanitychecks.py @@ -1,16 +1,6 @@ -<<<<<<< ours -||||||| ancestor -from pvecontrol.models.cluster import PVECluster -======= -from __future__ import annotations - -from typing import TYPE_CHECKING - ->>>>>>> theirs from pvecontrol.sanitycheck.checks import CheckCode from pvecontrol.sanitycheck.tests import DEFAULT_CHECKS, DEFAULT_CHECK_IDS from pvecontrol.models.cluster import PVECluster -from pvecontrol.models.cluster import PVECluster class SanityCheck: From a52dc7a81378e5a8b8623419c93c64c7dbf9f760 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:35:24 +0100 Subject: [PATCH 11/12] From 21da5b684c7ade058a8346f9679294545eb3db64 Mon Sep 17 00:00:00 2001 From: Laurent Corbes Date: Mon, 9 Mar 2026 21:01:38 +0100 Subject: [PATCH 12/12] docs: add CLAUDE.md with architecture and development guidance Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3c01eed1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`pvecontrol` is a Python CLI tool for managing Proxmox VE clusters. It wraps the [proxmoxer](https://pypi.org/project/proxmoxer/) library to provide higher-level operations not available in the Proxmox web UI (bulk VM listing, node evacuation/migration, sanity checks). + +## Development Setup + +```shell +# Activate the virtual environment +source .env/bin/activate + +# Run the tool +pvecontrol --help +``` + +To recreate the environment from scratch: + +```shell +python3 -m venv .env +.env/bin/pip install -r requirements.txt -r requirements-dev.txt -e . +``` + +## Commands + +```shell +# Run all tests +pytest src/ + +# Run a single test file +pytest src/tests/test_cluster.py + +# Lint +pylint src/pvecontrol/ + +# Format (line length 120) +black src/ +``` + +## Architecture + +The codebase follows a clean separation between CLI, business logic (actions), and domain models. + +**Entry point**: `src/pvecontrol/__init__.py` — defines the Click group `pvecontrol`, wires all subcommands, and exports `main()`. + +**CLI decorators**: `src/pvecontrol/cli.py` — reusable Click decorators (`with_table_options`, `task_related_command`, `migration_related_command`, `ResourceGroup`) shared across action modules. + +**Actions** (`src/pvecontrol/actions/`): One module per resource type (`cluster`, `node`, `vm`, `storage`, `task`). Each module defines Click commands and calls into models. Actions instantiate `PVECluster.create_from_config(cluster_name)` to get a connected cluster object. + +**Models** (`src/pvecontrol/models/`): Domain objects wrapping the Proxmox API: +- `PVECluster` — top-level object; holds `nodes`, `storages`, lazy-loaded `tasks`, `ha`, `backups`, `backup_jobs`; created via `create_from_config()` +- `PVENode` — holds a list of `PVEVm` instances; computes `allocatedcpu` / `allocatedmem` +- `PVEVm`, `PVEStorage`, `PVEVolume`, `PVETask`, `PVEBackupJob` — thin wrappers around API data + +**Configuration**: `src/pvecontrol/config.py` uses [confuse](https://confuse.readthedocs.io/) to load `~/.config/pvecontrol/config.yaml`. Supports `$()` shell command substitution in `user`, `password`, `token_name`, `token_value`, and `proxy_certificate` fields. + +**Output**: `src/pvecontrol/utils.py` — `print_output()` / `render_output()` render data via `prettytable` in text/json/csv/yaml/md formats. Memory/disk keys in `NATURALSIZE_KEYS` are automatically humanized. + +**Sanity checks** (`src/pvecontrol/sanitycheck/`): +- `checks.py` — abstract base `Check` class with `CheckCode` (OK/WARN/INFO/CRIT) and `CheckType` enums +- `tests/` — concrete check implementations: `Nodes`, `HaGroups`, `HaVms`, `VmsStartOnBoot`, `VmBackups`, `DiskUnused` +- `sanitychecks.py` — `SanityCheck` orchestrates running checks and displaying results; exits with code 1 on any CRIT + +**Tests** (`src/tests/`): Use `unittest` + `responses` for HTTP mocking. `PVEControlTestcase` in `testcase.py` provides a pre-wired cluster fixture with fake nodes, VMs, backups. Test fixtures live in `src/tests/fixtures/api.py`. + +## Conventions + +- Commits must follow [Angular Conventional Commits](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format) — releases are automated via `python-semantic-release` +- Allowed commit tags: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `style`, `refactor`, `test` +- All changes must go through a PR with review; `main` is protected +- Line length: 120 (black) / 150 (pylint)