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) 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..7c3cba38 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(), } 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: 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")