Skip to content
Open
73 changes: 73 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions src/pvecontrol/actions/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 7 additions & 1 deletion src/pvecontrol/models/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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(),
}
Expand Down
1 change: 1 addition & 0 deletions src/pvecontrol/models/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class VmStatus(Enum):
SUSPENDED = 3
POSTMIGRATE = 4
PRELAUNCH = 5
UNKNOWN = 6


class PVEVm:
Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/tests/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading