Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**vm_manager** is a Python tool for managing Virtual Machines on the SEAPATH platform. It operates in two modes, auto-detected at import time (`__init__.py`):
- **Standalone mode** (`vm_manager_libvirt.py`): manages VMs via KVM/libvirt only
- **Cluster mode** (`vm_manager_cluster.py`): manages VMs on a Pacemaker HA cluster with Ceph RBD storage

## Build & Install

```bash
# Install locally (or use cqfd for containerized builds)
pip install .

# Using cqfd (Docker-based build wrapper, config in .cqfdrc)
cqfd
```

## Linting & Formatting

```bash
# Format with Black (line length 79, Python 3.8 target)
black -l 79 -t py38 .

# Check formatting without modifying
black -l 79 -t py38 --check .

# Flake8
python3 -m flake8 --ignore=E501,W503

# Pylint
pylint pacemaker_helper rbd_helper vm_manager

# Via cqfd flavors
cqfd -b check_format # check formatting
cqfd -b format # auto-format
cqfd -b flake # flake8
cqfd -b check # pylint
```

## Tests

Tests are integration scripts requiring a real Ceph/Pacemaker cluster. Run individually:

```bash
python3 -m vm_manager.helpers.tests.rbd_manager.clone_rbd
python3 -m vm_manager.helpers.tests.pacemaker.add_vm
```

Test scripts are in `vm_manager/helpers/tests/pacemaker/` and `vm_manager/helpers/tests/rbd_manager/`.

## Architecture

**Entry points** (defined in `pyproject.toml [project.scripts]`):
- `vm_manager_cmd` — CLI (argparse) exposing all VM operations as subcommands
- `libvirt_cmd` — Standalone CLI for libvirt-only operations
- `vm_manager_api.py` — Flask REST API (`/`, `/status/<guest>`, `/stop/<guest>`, `/start/<guest>`)

**Mode selection** (`__init__.py`): tries to import `RbdManager` and `Pacemaker`. If both succeed, public API functions (`list_vms`, `create`, `remove`, `start`, `stop`, etc.) come from `vm_manager_cluster.py`; otherwise from `vm_manager_libvirt.py`.

**Helper classes** (all used as context managers with `with` statements):
- `LibVirtManager` (`helpers/libvirt.py`): wraps `libvirt-python` for domain management
- `Pacemaker` (`helpers/pacemaker.py`): wraps `crm` CLI via `subprocess.run()`
- `RbdManager` (`helpers/rbd_manager.py`): wraps Ceph `rados`/`rbd` Python bindings

## Conventions

- **Python target**: 3.8+
- **Line length**: 79 characters
- **License**: Apache-2.0 (all source files have copyright headers)
- **Code review**: Gerrit (`.gitreview` → `g1.sfl.team`)
- **VM naming**: alphanumeric only, validated by `_check_name()`
- **Disk naming**: system disks prefixed with `system_` (`OS_DISK_PREFIX` constant)
- **Flake8 config** (`.flake8`): ignores F401 in `__init__.py`, E501 and W503 globally
- **Custom exceptions**: `RbdException`, `PacemakerException`
1 change: 1 addition & 0 deletions vm_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@
start,
stop,
status,
autostart,
)
8 changes: 8 additions & 0 deletions vm_manager/helpers/libvirt.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ def undefine(self, vm_name):
domain = self._conn.lookupByName(vm_name)
domain.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_NVRAM)

def set_autostart(self, vm_name, enabled):
"""
Set the autostart flag on a VM
:param vm_name: the VM name
:param enabled: True to enable autostart, False to disable
"""
self._conn.lookupByName(vm_name).setAutostart(1 if enabled else 0)

def start(self, vm_name):
"""
Start a VM
Expand Down
2 changes: 2 additions & 0 deletions vm_manager/helpers/libvirt_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import argparse
from vm_manager.helpers.libvirt import LibVirtManager


def main():
parser = argparse.ArgumentParser(description="libvirt helper cli wrapper")
subparsers = parser.add_subparsers(
Expand Down Expand Up @@ -48,5 +49,6 @@ def main():
elif args.command == "export":
LibVirtManager.export_configuration(args.domain, args.destination)


if __name__ == "__main__":
main()
10 changes: 6 additions & 4 deletions vm_manager/helpers/pacemaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,12 @@ def find_resource(resource):
:return: the node where the resource is running or None if the resource
is not running or not found
"""
command = "crm status | " + \
f'grep -E "^ \* {resource}\\b" | ' + \
"grep Started | " + \
"awk 'NF>1{print $NF}'"
command = (
"crm status | "
+ f'grep -E "^ \* {resource}\\b" | '
+ "grep Started | "
+ "awk 'NF>1{print $NF}'"
)
ret = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
host = ret.stdout.decode().strip()
if host == "":
Expand Down
2 changes: 1 addition & 1 deletion vm_manager/helpers/rbd_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def create_image(self, img, size, overwrite=True):
if not isinstance(size, int):
unit_list = ["B", "K", "M", "G", "T"]
i = unit_list.index(size[-1])
size = int(size[:-1]) * 1024**i
size = int(size[:-1]) * 1024 ** i

if self.image_exists(img):
if overwrite:
Expand Down
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/pacemaker/add_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MIGRATE_FROM_TIMEOUT = "60"
MIGRATE_TO_TIMEOUT = "120"


def main():
with Pacemaker(VM_NAME) as p:

Expand Down Expand Up @@ -49,5 +50,6 @@ def main():

p.wait_for("Started")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/pacemaker/remove_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
SLEEP = 1
VM_NAME = "vm1"


def main():
with Pacemaker(VM_NAME) as p:

Expand All @@ -36,5 +37,6 @@ def main():
if VM_NAME in resources:
raise Exception("Resource " + VM_NAME + " was not removed")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/pacemaker/start_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
VM_NAME = "vm1"
SLEEP = 1


def main():

with Pacemaker(VM_NAME) as p:
Expand All @@ -26,5 +27,6 @@ def main():
else:
raise Exception("VM is already started")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/pacemaker/stop_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
VM_NAME = "vm1"
SLEEP = 1


def main():
with Pacemaker(VM_NAME) as p:

Expand All @@ -25,5 +26,6 @@ def main():
else:
raise Exception("Machine is already stopped")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/clone_rbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
SNAP = "snap"
TEXT = "Hello world"


def main():
with RbdManager(CEPH_CONF, POOL_NAME) as rbd:

Expand Down Expand Up @@ -119,5 +120,6 @@ def main():
rbd.remove_image(img)
print("Image list: " + str(rbd.list_images()))


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/create_rbd_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
IMG_NAME = "img1"
GROUP = "group1"


def main():

with RbdManager(CEPH_CONF, POOL_NAME) as rbd:
Expand Down Expand Up @@ -105,5 +106,6 @@ def main():
rbd.remove_image(IMG_NAME)
print("Image list: " + str(rbd.list_images()))


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/create_rbd_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
NS1 = "namespace1"
NS2 = "namespace2"


def main():

with RbdManager(CEPH_CONF, POOL_NAME) as rbd:
Expand Down Expand Up @@ -129,5 +130,6 @@ def main():
rbd.remove_namespace(ns)
print("Namespace list: " + str(rbd.list_namespaces()))


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/metadata_rbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test4": "metadatatest4",
}


def main():

with RbdManager(CEPH_CONF, POOL_NAME) as rbd:
Expand Down Expand Up @@ -71,5 +72,6 @@ def main():
rbd.remove_image(IMG_NAME)
print("Image list: " + str(rbd.list_images()))


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/purge_rbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SNAPS = ["snap0", "snap1", "snap2", "snap3", "snap4", "snap5"]
GROUP = "group1"


def main():

with RbdManager(CEPH_CONF, POOL_NAME) as rbd:
Expand Down Expand Up @@ -163,5 +164,6 @@ def main():
rbd.remove_image(IMG_NAME) # remove forces purge
print("Image list: " + str(rbd.list_images()))


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions vm_manager/helpers/tests/rbd_manager/rollback_rbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,6 @@ def main():
finally:
cleanup(rbd)


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions vm_manager/helpers/tests/rbd_manager/write_rbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
IMG_NAME = "img1"
TEXT = "Hello world"


def main():

with RbdManager(CEPH_CONF, POOL_NAME) as rbd:
Expand Down Expand Up @@ -60,5 +61,6 @@ def main():
rbd.remove_image(IMG_NAME)
print("Image list: " + str(rbd.list_images()))


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions vm_manager/vm_manager_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ def start_vm(guest):
def main():
app.run(host="0.0.0.0")


if __name__ == "__main__":
main()
Loading