From ed79bca0acfe640f856b84c7ad97cfd3dde061ca Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 29 Jul 2026 22:23:22 +0000 Subject: [PATCH] docs: add agent-oriented knowledge base (AGENTS.md + .kb/) Adopt the canonical-agents convention: AGENTS.md directory indexes and distilled .kb/*.md knowledge notes spanning the dev workflow, configuration, BaseCloud/BaseInstance architecture, per-cloud backend specifics, testing, examples, and Sphinx docs. Content describes architecture and references self-documenting source modules rather than restating method signatures. --- .kb/adding-a-cloud.md | 29 +++++++++++++++ .kb/agents.md | 45 +++++++++++++++++++++++ .kb/cloud-abstraction.md | 27 ++++++++++++++ .kb/configuration.md | 23 ++++++++++++ .kb/development.md | 25 +++++++++++++ .kb/ssh-and-instances.md | 23 ++++++++++++ AGENTS.md | 44 ++++++++++++++++++++++ VERSION | 2 +- docs/.kb/documentation.md | 24 ++++++++++++ examples/.kb/examples.md | 18 +++++++++ pycloudlib/AGENTS.md | 39 ++++++++++++++++++++ pycloudlib/azure/.kb/azure.md | 24 ++++++++++++ pycloudlib/ec2/.kb/ec2.md | 24 ++++++++++++ pycloudlib/gce/.kb/gce.md | 23 ++++++++++++ pycloudlib/ibm/.kb/ibm.md | 24 ++++++++++++ pycloudlib/ibm_classic/.kb/ibm-classic.md | 24 ++++++++++++ pycloudlib/lxd/.kb/lxd.md | 24 ++++++++++++ pycloudlib/oci/.kb/oci.md | 24 ++++++++++++ pycloudlib/openstack/.kb/openstack.md | 24 ++++++++++++ pycloudlib/qemu/.kb/qemu.md | 23 ++++++++++++ pycloudlib/vmware/.kb/vmware.md | 23 ++++++++++++ tests/.kb/testing.md | 24 ++++++++++++ tests/AGENTS.md | 16 ++++++++ 23 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 .kb/adding-a-cloud.md create mode 100644 .kb/agents.md create mode 100644 .kb/cloud-abstraction.md create mode 100644 .kb/configuration.md create mode 100644 .kb/development.md create mode 100644 .kb/ssh-and-instances.md create mode 100644 AGENTS.md create mode 100644 docs/.kb/documentation.md create mode 100644 examples/.kb/examples.md create mode 100644 pycloudlib/AGENTS.md create mode 100644 pycloudlib/azure/.kb/azure.md create mode 100644 pycloudlib/ec2/.kb/ec2.md create mode 100644 pycloudlib/gce/.kb/gce.md create mode 100644 pycloudlib/ibm/.kb/ibm.md create mode 100644 pycloudlib/ibm_classic/.kb/ibm-classic.md create mode 100644 pycloudlib/lxd/.kb/lxd.md create mode 100644 pycloudlib/oci/.kb/oci.md create mode 100644 pycloudlib/openstack/.kb/openstack.md create mode 100644 pycloudlib/qemu/.kb/qemu.md create mode 100644 pycloudlib/vmware/.kb/vmware.md create mode 100644 tests/.kb/testing.md create mode 100644 tests/AGENTS.md diff --git a/.kb/adding-a-cloud.md b/.kb/adding-a-cloud.md new file mode 100644 index 00000000..12652275 --- /dev/null +++ b/.kb/adding-a-cloud.md @@ -0,0 +1,29 @@ +# Preface + +End-to-end checklist for adding a new cloud backend to pycloudlib. Read before starting a new backend; cross-reference `.kb/cloud-abstraction.md` for the contract. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +A new backend is a subpackage under `pycloudlib//` providing a concrete `BaseCloud` subclass and a concrete `BaseInstance` subclass, wired into the public API, build, config template, docs, examples, and tests. Each step below mirrors how existing backends are structured. + + +# Important + +1. **Subpackage** `pycloudlib//` with `__init__.py`, `cloud.py` (`(BaseCloud)`), `instance.py` (`Instance(BaseInstance)`). Add `errors.py`/`util.py` only if needed; cloud-specific exceptions MUST inherit from `PycloudlibException` (root in `pycloudlib/errors.py`). +2. **Implement every abstract method** of `BaseCloud` and `BaseInstance` (read `cloud.py`/`instance.py` for the list). If a method is genuinely unsupported, raise `PycloudlibError` (see `Openstack.released_image`). +3. **Register the class** in `pycloudlib/__init__.py` (import + `__all__` entry). +4. **Add SDK deps** to `pyproject.toml` `[project.dependencies]` and, if the SDK lacks type stubs, add it to the `[[tool.mypy.overrides]] ignore_missing_imports` list. Prefer strict typing where possible. +5. **Config template**: add a `[]` section to `pycloudlib.toml.template` mirroring constructor kwargs; uncommented keys = required. Update `.kb/configuration.md` if precedence behavior changes. +6. **Docs**: add `docs/clouds/.md` (user-facing) and `docs/source/pycloudlib..*.rst` per module (Sphinx autodoc). Add the cloud to the `docs/clouds` toctree glob if not already covered by `clouds/*`. +7. **Example**: add `examples/.py` demonstrating launch/snapshot/cleanup. See `examples/.kb/examples.md`. +8. **Tests**: add `tests/unit_tests//` with mocked SDK tests (CI runs these). Add `tests/integration_tests//` for live tests, gated by pytest markers `ci`/`main_check` as appropriate (see `tests/.kb/testing.md`). +9. **Knowledge base**: create `pycloudlib//.kb/.md` capturing config keys, SDK auth flow, cloud-specific methods, and gotchas; link it from `pycloudlib/AGENTS.md` _Documents_. +10. **Verify**: `tox -e ruff`, `tox -e mypy`, `tox -e py38` (or `make test`), and `tox -e docs` (Sphinx with `-W` treats warnings as errors, so missing autodoc targets will fail the build). + + +# Architecture + +For the abstract contract each backend must satisfy, read `cloud.py`/`instance.py` directly and see `.kb/cloud-abstraction.md` for the architectural intent. The steps above are the wiring checklist around that contract; this article intentionally does not restate the API surface. diff --git a/.kb/agents.md b/.kb/agents.md new file mode 100644 index 00000000..cae1f4e0 --- /dev/null +++ b/.kb/agents.md @@ -0,0 +1,45 @@ +# Preface + +This repository follows strict conventions for organizing agent-oriented knowledge documents, and this document is required reading for any agent that wants to read or write these. The agent-oriented knowledge base is distinct from the user-facing Sphinx documentation under `docs/`; the latter targets library users, while `.kb/*.md` targets agents working on the codebase. + +Read the top-level `.kb/agents.md` file before continuing below. + +# Overview + +Every directory in this repository, including the root, may have its own `AGENTS.md` file and `.kb/` subdirectory. The `AGENTS.md` file gives a general view of the directory, while more specific knowledge lives in `.kb/*.md` files with dashed lowercase names (e.g. `.kb/cloud-abstraction.md`). + +The design of this structure has the following key goals: + +- **Mechanical** - Agents are the main actors reading and writing the knowledge base. +- **Generic** - Benefits any agentic workflow, no matter the editor or platform. +- **Distilled** - Avoids verbose task or plan logs that pollute the context window. +- **Hierarchical** - Avoids excessive information in a single place that also pollutes the context window. +- **Human** - Information is readily available and reviewable in a friendly format. + + +# Important + +- Read local `AGENTS.md` files upon navigating into a directory. +- Keep the `.kb/*.md` files updated whenever there is something relevant to be documented or updated; remove content that becomes stale. +- Follow the header conventions outlined below. Only the _Preface_ header is required; omit other headers if empty or trivial. +- Do NOT duplicate content that lives in source docstrings or the Sphinx `docs/` tree. Reference it instead. +- File names use dashed-lowercase (`some-topic.md`), matching pycloudlib's existing doc style. + + +# Headers + +The following are the ONLY top-level headers allowed across the `.kb/*.md` files in this repository, to maintain semantic standardization across projects. Sub-headers are okay. + +- _Preface_ - A brief introduction outlining the scope and relevance of a specific `*.md` file. This section MUST be at the top of every `.kb/*.md` file so agents can easily grep for it, and the last line of this section MUST be "Read the top-level `.kb/agents.md` file before continuing below." so rules are followed. +- _Overview_ - High-level summary of the directory, subsystem or knowledge base layout at large. Do NOT use this to list files or directories. +- _Important_ - Essential directives for the agent outlining critical constraints, behaviors, or rules. +- _Headers_ - Global registry of header definitions, uniquely hosted at the root `.kb/agents.md` (this file). Do NOT use this header in any other document. +- _Architecture_ - Structural design details or boundary explanations for a given component. Only use this for software architecture concepts, NOT for directory outlining. Also avoid using this as a code reference (keep that in the code itself). +- _Directory_ - Only in `AGENTS.md` files, it briefly outlines the contents and structure of the directory the `AGENTS.md` file is in, and potentially nested small directories that do not justify their own `AGENTS.md` file. For `.kb/*.md` files, use _Documents_ instead. +- _Documents_ - Only in `AGENTS.md` files, it outlines the content of `.kb/*.md` files and also immediately nested `AGENTS.md` child files, to aid agent navigation. It MUST be the last section in the file. It's okay to also mention such filenames inline when they are relevant elsewhere in the text. + +For the _Directory_ and _Documents_ listings, format items as a dashed list starting with the file or directory name surrounded by backticks, a dash, and then a brief description: + +``` +- `filename` - Terse summary. +``` diff --git a/.kb/cloud-abstraction.md b/.kb/cloud-abstraction.md new file mode 100644 index 00000000..dacd20f1 --- /dev/null +++ b/.kb/cloud-abstraction.md @@ -0,0 +1,27 @@ +# Preface + +Architectural intent behind the `BaseCloud` / `BaseInstance` abstractions in `pycloudlib/cloud.py` and `pycloudlib/instance.py`. Read before auditing cross-cloud behavior or deciding where new functionality belongs; for the concrete method list and signatures, read the base classes directly. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`BaseCloud` and `BaseInstance` are ABCs defining a uniform surface every backend implements. The point of the abstraction is that cloud-agnostic callers work unchanged across backends; backends subclass and add cloud-specific helpers without exposing backend-specific shapes to generic code. + + +# Important + +- Read `cloud.py` and `instance.py` for the authoritative abstract method list and `__init__` signatures — they are the source of truth and this note intentionally does not restate them. +- Both base classes are context managers whose `__exit__` calls cleanup and raises `CleanupError` on failure. `CleanupError` usually means leaked resources — do not silently swallow it. +- Backends track created instances/images in `created_instances`/`created_images` and extend `clean()` (calling `super().clean()` first) for cloud-specific resources (e.g. EC2 VPCs/keys, LXD profiles/snapshots). Generic `launch`/`snapshot` callers rely on this tracking for automatic cleanup. +- `ImageType` (in `cloud.py`) dispatches image flavors on clouds that support them (Azure, EC2, GCE, LXD); other clouds accept and ignore it via `**kwargs`. +- Not every abstract method is meaningfully supported by every cloud: some raise `PycloudlibError` (e.g. `Openstack.released_image`/`daily_image`) or fall back to a sibling (e.g. `IBMClassic.daily_image` → `released_image`). Cloud-agnostic code must tolerate this. +- The `_type` class attribute identifies the backend for logging and helpers. + + +# Architecture + +- `BaseInstance` exec is paramiko-based for most clouds, but LXD/QEMU/VMWare translate a non-SSH transport (the `lxc` CLI, a QMP socket, `govc`) into the same `execute`/`run`/`Result` surface. See `.kb/ssh-and-instances.md` for the `Result`/paramiko model. +- Tag validation lives in `BaseCloud._validate_tag` with per-cloud override rules; invalid tags raise `InvalidTagNameError(tag, rules_failed)`. +- Configuration is resolved in `BaseCloud.__init__` and shared with instances via the `key_pair`; see `.kb/configuration.md` for precedence. \ No newline at end of file diff --git a/.kb/configuration.md b/.kb/configuration.md new file mode 100644 index 00000000..b3c0f32f --- /dev/null +++ b/.kb/configuration.md @@ -0,0 +1,23 @@ +# Preface + +How pycloudlib resolves per-cloud configuration. Read before changing precedence or debugging "must be defined in pycloudlib.toml" errors; for per-cloud keys read `pycloudlib.toml.template` and each cloud's `__init__` docstring. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +Configuration is layered: explicit constructor kwargs > `pycloudlib.toml` values > cloud SDK defaults / env vars. `config.py` parses the TOML; `pycloudlib.toml.template` is the authoritative per-cloud key reference. + + +# Important + +- In `pycloudlib.toml.template`, **uncommented keys are required**, commented keys are optional with shown defaults. Do not check a filled-in `pycloudlib.toml` into version control; secrets belong in a secret manager. +- `Config` (in `config.py`) subclasses `dict` only to raise a clearer `KeyError` on missing-key `__getitem__` access; code using `.get(key)` gets `None` and must handle it. Don't rely on this difference leaking into call sites. +- When adding a cloud, add a `[]` section to `pycloudlib.toml.template` mirroring its constructor kwargs. + + +# Architecture + +- `parse_config` (in `config.py`) tries, in order: the `config_file` constructor arg > `$PYCLOUDLIB_CONFIG` > `~/.config/pycloudlib.toml` > `/etc/pycloudlib.toml`. First existing, parseable file wins; later paths are not merged. Read `config.py` for the exact `CONFIG_PATHS` list and `ConfigFile` type. +- Each cloud's `__init__` resolves each value as `kwarg or self.config.get("key") or default` before constructing its SDK client; `BaseCloud.required_values` only validates that at least one supplied value is non-None. See each `pycloudlib//.kb/.md` for that cloud's auth flow. \ No newline at end of file diff --git a/.kb/development.md b/.kb/development.md new file mode 100644 index 00000000..937cdbbd --- /dev/null +++ b/.kb/development.md @@ -0,0 +1,25 @@ +# Preface + +Dev environment, lint, typecheck, and test commands for pycloudlib. Read before running checks; for env/flag specifics read `tox.ini`, `Makefile`, and `pyproject.toml` directly. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +pycloudlib uses `uv` (wrapped by a thin `Makefile`) and `tox` for the multi-check workflow. Supported Python range is >=3.8; CI covers 3.8/3.10/3.12. ruff handles lint+format, mypy handles typing, pytest runs unit tests with `--doctest-modules`. + + +# Important + +- After editing any Python in `pycloudlib/` or `examples/`, run ruff and mypy. Prefer `make test` (full tox) or the individual tox envs defined in `tox.ini`. +- Default `tox` envlist only **checks** (does not reformat): `ruff`, `mypy`, `py38`. Use `tox -e format` to apply formatting. +- Unit tests live in `tests/unit_tests/` and run in CI; integration tests in `tests/integration_tests/` need live cloud credentials and are marker-gated (see `tests/.kb/testing.md`). +- New code: prefer Markdown for new docs (per `docs/contributing.md`), follow ruff's pep257 docstring convention, and keep PRs to a single issue. + + +# Architecture + +- `pyproject.toml` is the source of truth for dependencies, ruff/mypy/pytest config, and build (hatchling). mypy has `ignore_missing_imports` overrides for many cloud SDKs and `check_untyped_defs = false` for a known TODO module list — read the `[tool.mypy.overrides]` sections rather than assuming; prefer fixing typing over widening the relaxed list. +- `tox.ini` defines all envs (`pytest`, `py38`/`py310`/`py312`, `mypy`, `ruff`, `format`, `docs`, `integration-tests*`); read it for exact commands/flags. +- `Makefile` wraps `uv` (`build`/`install`/`test`/`venv`/`clean`/`publish`); `uv.lock` pins the dependency set; `VERSION` is read by hatchling. \ No newline at end of file diff --git a/.kb/ssh-and-instances.md b/.kb/ssh-and-instances.md new file mode 100644 index 00000000..94a55b09 --- /dev/null +++ b/.kb/ssh-and-instances.md @@ -0,0 +1,23 @@ +# Preface + +SSH key handling, paramiko usage in `BaseInstance`, and the `Result` exec model. Read when debugging instance connectivity or key wiring; for signatures and defaults read `instance.py`/`key.py`/`result.py` directly. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`BaseInstance` reaches instances over SSH via paramiko using a `KeyPair`; commands return a `Result`. SSH clients are lazily opened and reused for the instance lifetime. + + +# Important + +- Do not log or echo key material. `KeyPair.__str__` includes paths but not key contents; preserve that boundary when handling keys. +- `Result` is a `str` subclass whose string value is stdout, so `bool(result)` reflects success and `if not result:` detects failure. See `result.py` for the full attribute surface; do not assume attributes beyond what's defined there. +- Backends that do not use paramiko for transport (LXD: `lxc` CLI via `subp`; QEMU: QMP socket; VMWare: `govc`) still expose the same `execute`/`run`/`Result` surface for cloud-agnostic callers, translating their transport into a `Result`. See each `pycloudlib//.kb/.md`. + + +# Architecture + +- `BaseInstance` holds cached, lazily-initialized `_ssh_client`/`_sftp_client` reused across `execute`/`run`/file transfer; the connect logic (with its paramiko exception handling and `boot_timeout`/`ready_timeout` retry) lives in `instance.py`. +- `BaseCloud` builds `self.key_pair` in `__init__` from config (`public_key_path`/`private_key_path`/`key_name`); `KeyPair` path/`public_key_content`/`UnsetSSHKeyError` behavior is defined in `key.py`. A cloud may expose `use_key(...)` to swap keys at runtime — consult the cloud's `cloud.py`. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3ee21e7d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# Preface + +pycloudlib is a Python library (Python >=3.8) that launches, interacts with, and snapshots Ubuntu cloud instances across many public and private clouds through a uniform `BaseCloud` / `BaseInstance` abstraction. This file is relevant to any task touching the source tree, build, tests, docs, or examples. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +The package supports Azure, EC2, GCE, IBM VPC, IBM Classic, LXD (containers and VMs), OCI, Openstack, QEMU, and VMWare. Each cloud lives in its own subpackage under `pycloudlib//` and implements the abstract API defined in `pycloudlib/cloud.py` and `pycloudlib/instance.py`. The library is consumed both as a library (via `pycloudlib` top-level exports) and via the example scripts under `examples/`. + + +# Architecture + +- `BaseCloud` (`pycloudlib/cloud.py`) and `BaseInstance` (`pycloudlib/instance.py`) define the abstract contract every backend satisfies; read them for the method list, and see `.kb/cloud-abstraction.md` for architectural intent. +- Each `` subpackage provides a concrete cloud class and instance class plus optional helpers (`vpc.py`, `util.py`, `errors.py`, `_util.py`). +- Cross-cutting modules live at the package root: `config.py` (TOML config), `key.py` (`KeyPair`), `result.py` (`Result`), `errors.py` (root exception hierarchy), `util.py` (release maps, shell helpers), `constants.py`. +- Configuration is layered: constructor kwargs > `pycloudlib.toml` (see `pycloudlib.toml.template`) > cloud SDK defaults / env vars; see `.kb/configuration.md`. + +See `.kb/cloud-abstraction.md` for the abstract API surface agents should rely on, and each `pycloudlib//.kb/.md` for backend-specific behavior and gotchas. + + +# Directory + +- `pycloudlib/` - The Python package; see `pycloudlib/AGENTS.md` for module-level detail. +- `tests/` - Unit tests (`unit_tests/`, run in CI) and integration tests (`integration_tests/`, require live cloud credentials). See `tests/AGENTS.md`. +- `examples/` - Runnable scripts demonstrating each cloud's API. See `examples/.kb/examples.md`. +- `docs/` - Sphinx user-facing documentation (MyST Markdown + reStructuredText). See `docs/.kb/documentation.md`. +- `pyproject.toml` - Build (hatchling), dependencies, ruff/mypy/pytest config. +- `tox.ini` - Tox environments: `ruff`, `mypy`, `py38`/`py310`/`py312` (pytest), `format`, `docs`, `integration-tests*`. +- `Makefile` - Thin wrappers around `uv` (`build`, `install`, `test`, `venv`, `clean`). +- `pycloudlib.toml.template` - Reference template for the per-cloud config file users copy to `~/.config/pycloudlib.toml` or `/etc/pycloudlib.toml`. +- `VERSION` - Single-line version read by hatchling. +- `uv.lock` - Locked dependency set for the `uv` workflow. + + +# Documents + +- `.kb/agents.md` - General rules for the knowledge base reading and writing. +- `.kb/development.md` - Dev environment, lint/typecheck/test commands, and Python version coverage. +- `.kb/configuration.md` - `pycloudlib.toml` resolution precedence and per-cloud required keys. +- `.kb/cloud-abstraction.md` - The `BaseCloud` / `BaseInstance` contract agents should rely on across backends. +- `.kb/ssh-and-instances.md` - SSH key handling, paramiko usage, and the `Result` exec model. +- `.kb/adding-a-cloud.md` - Steps to add a new cloud backend end-to-end. diff --git a/VERSION b/VERSION index 0e8f0936..37a82308 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1!11.1.3 +1!11.1.4 diff --git a/docs/.kb/documentation.md b/docs/.kb/documentation.md new file mode 100644 index 00000000..67687a42 --- /dev/null +++ b/docs/.kb/documentation.md @@ -0,0 +1,24 @@ +# Preface + +The `docs/` tree is the user-facing Sphinx documentation (distinct from the agent-oriented `.kb/` knowledge base). Read before editing docs or the Sphinx build; for config read `docs/conf.py` and `docs/index.rst`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +Sphinx builds MyST Markdown (`.md`) and reStructuredText (`.rst`) into the published docs at pycloudlib.readthedocs.io. Per-cloud guides live in `docs/clouds/*.md`; API autodoc stubs in `docs/source/pycloudlib..rst`; the index/config in `docs/index.rst`/`docs/conf.py`. + + +# Important + +- Build with `tox -e docs` (separate env because it `cd`s into `docs/`); it runs `sphinx-build ... -W`, so **warnings are errors** — a missing autodoc target or broken cross-reference fails the build. +- Prefer **Markdown** for new prose docs (per `docs/contributing.md`); use `.rst` only where Sphinx autodoc tooling expects it. +- The `.kb/` knowledge base and `AGENTS.md` files are NOT part of the Sphinx toctree (they live outside `docs/` and are agent-oriented only); do not link them from `docs/index.rst`. +- `docs/_build/` is generated output — never edit it (cleaned by `make -C docs clean` / root `make clean`). + + +# Architecture + +- `index.rst` defines the toctree (read it for the exact groups); `docs/clouds/*` and `docs/examples/*` are globs that pick up new files automatically, so adding a cloud's `docs/clouds/.md` and `docs/source/pycloudlib..*.rst` is sufficient for autodoc. +- `docs/conf.py` inserts `..` into `sys.path` so autodoc can import `pycloudlib`; do not break that path setup. `docs/_static/`/`docs/_templates/` hold theme assets. \ No newline at end of file diff --git a/examples/.kb/examples.md b/examples/.kb/examples.md new file mode 100644 index 00000000..79843848 --- /dev/null +++ b/examples/.kb/examples.md @@ -0,0 +1,18 @@ +# Preface + +The `examples/` directory holds runnable scripts demonstrating each cloud's API. Read before adding an example; the demo pattern is documented in `examples/base_api.py`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +Each cloud has a top-level `examples/.py` demonstrating launch, exec, snapshot, and cleanup. `base_api.py` is a shared helper that exercises the `BaseCloud` API surface generically; per-cloud scripts wire a concrete `pycloudlib.` client into it. + + +# Important + +- New examples should follow the `base_api.py` shape: construct a cloud client with a tag and config, call `exercise_api`, and let the cloud's context manager clean up. Avoid ad-hoc cleanup that diverges from the library's intended usage. +- Examples are imported by `tox -e mypy` (`mypy pycloudlib examples`) and by `tox -e ruff`, so they must stay ruff-clean and type-check. +- `examples/oracle/` contains OCI-specific multi-instance demos; the top-level `oracle.py` is the basic single-instance demo. Mirror this split if a cloud needs both. +- Examples use `#cloud-config` user-data for cloud-init; keep cloud-config hermetic and idempotent. \ No newline at end of file diff --git a/pycloudlib/AGENTS.md b/pycloudlib/AGENTS.md new file mode 100644 index 00000000..00813aab --- /dev/null +++ b/pycloudlib/AGENTS.md @@ -0,0 +1,39 @@ +# Preface + +This directory is the `pycloudlib` Python package. Its root modules define the cross-cutting abstractions and helpers shared by every cloud backend; each `/` subpackage implements a concrete backend. Read this when working on the abstract API or shared infrastructure, or before navigating into a specific backend. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +The package root holds the abstract base classes (`cloud.py`, `instance.py`), configuration (`config.py`), SSH keys (`key.py`), command results (`result.py`), the exception hierarchy (`errors.py`), release/utility helpers (`util.py`), and constants (`constants.py`). Concrete backends live in subpackages and are re-exported from `__init__.py`. + + +# Directory + +- `__init__.py` - Re-exports the concrete cloud classes; this is the public API surface (read it for the `__all__` list). +- `cloud.py` - `BaseCloud` ABC and the `ImageType` enum (see `.kb/cloud-abstraction.md` for the architectural intent). +- `instance.py` - `BaseInstance` ABC (SSH exec, file transfer, lifecycle, snapshotting). +- `config.py` - TOML config parsing (`Config`, `parse_config`, `CONFIG_PATHS`); see `.kb/configuration.md`. +- `key.py` - `KeyPair` (SSH key paths, name, content). +- `result.py` - `Result` (the exec return type, a `str` subclass); see `.kb/ssh-and-instances.md`. +- `errors.py` - Root `PycloudlibException` hierarchy; each cloud may add its own `errors.py`. +- `util.py` - Release maps (`UBUNTU_RELEASE_VERSION_MAP`, `LTS_RELEASES`), `subp`, shell/tag helpers. +- `constants.py` - Shared constants (e.g. `LOCAL_UBUNTU_ARCH`). +- `azure/`, `ec2/`, `gce/`, `ibm/`, `ibm_classic/`, `lxd/`, `oci/`, `openstack/`, `qemu/`, `vmware/` - Backend subpackages; each has its own `.kb/.md` knowledge note. +- `py.typed` - PEP 561 marker enabling type-checker consumption of the package. + + +# Documents + +- `azure/.kb/azure.md` - Azure backend specifics (config keys, clients, gotchas). +- `ec2/.kb/ec2.md` - EC2 backend specifics, including VPC handling. +- `gce/.kb/gce.md` - GCE backend specifics. +- `ibm/.kb/ibm.md` - IBM VPC backend specifics. +- `ibm_classic/.kb/ibm-classic.md` - IBM Classic backend specifics. +- `lxd/.kb/lxd.md` - LXD backend specifics (containers vs VMs). +- `oci/.kb/oci.md` - OCI backend specifics. +- `openstack/.kb/openstack.md` - Openstack backend specifics. +- `qemu/.kb/qemu.md` - QEMU backend specifics. +- `vmware/.kb/vmware.md` - VMWare backend specifics. diff --git a/pycloudlib/azure/.kb/azure.md b/pycloudlib/azure/.kb/azure.md new file mode 100644 index 00000000..237800d5 --- /dev/null +++ b/pycloudlib/azure/.kb/azure.md @@ -0,0 +1,24 @@ +# Preface + +Azure backend specifics for `pycloudlib.azure`. Read before editing `azure/`; for config keys, constructor kwargs, and image dicts read `azure/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`Azure(BaseCloud)` uses the Azure mgmt SDK with service-principal credentials; `AzureInstance(BaseInstance)` wraps a compute VM plus its network interface. Azure has the richest `ImageType` flavor support (generic, minimal, Pro, Pro FIPS, Pro FIPS Updates, CVM). + + +# Important + +- Credentials can be obtained via `az ad sp create-for-rbac --sdk-auth`; otherwise read from `~/.azure`. See `pycloudlib.toml.template` `[azure]` for the required/optional keys. +- Image selection dispatches `ImageType` to module-level URN dicts in `cloud.py`; when adding a release, update every relevant dict rather than relying on a single map. +- mypy has `check_untyped_defs = false` for `pycloudlib.azure.cloud`/`instance` (see the TODO overrides in `pyproject.toml`); prefer fixing typing over widening this. + + +# Architecture + +- Three SDK clients are constructed (`ComputeManagementClient`/`NetworkManagementClient`/`ResourceManagementClient`); the instance holds the compute + network clients and created VM/NIC objects. Helpers (security profile models, API version constants, nested-update util) live in `azure/security_types.py` and `azure/util.py`. +- Resources are tagged with `self.tag` and grouped under a resource group (`azure/util.py` `AzureParams`); `clean()` extends `BaseCloud.clean()` for Azure-specific teardown. +- The Azure SDK HTTP logging policy is silenced to reduce noise. \ No newline at end of file diff --git a/pycloudlib/ec2/.kb/ec2.md b/pycloudlib/ec2/.kb/ec2.md new file mode 100644 index 00000000..c6775a56 --- /dev/null +++ b/pycloudlib/ec2/.kb/ec2.md @@ -0,0 +1,24 @@ +# Preface + +EC2 backend specifics for `pycloudlib.ec2`. Read before editing `ec2/`; for config keys, constructor kwargs, and method behavior read `ec2/cloud.py`, `ec2/instance.py`, and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`EC2(BaseCloud)` authenticates via boto3/botocore (profile, access key pair, or `~/.aws`) and manages instances plus optional custom VPCs. `EC2Instance(BaseInstance)` wraps a boto3 EC2 resource. EC2 supports `ImageType` flavors and custom VPC creation/reuse via `VPC` (`ec2/vpc.py`). + + +# Important + +- Auth falls back to `~/.aws`; missing region/credentials raise `CloudSetupError` with guidance — read `ec2/util.py` `_get_session` and `ec2/cloud.py` for the resolution flow. See `pycloudlib.toml.template` `[ec2]` for keys. +- `NO_GP3_RELEASES` (in `ec2/cloud.py`) lists releases predating the gp3 disk type; launch logic must respect it when selecting the root volume. +- mypy: boto3/botocore are in the `ignore_missing_imports` overrides (`pyproject.toml`); do not rely on their types without local stubs. + + +# Architecture + +- Two boto3 handles are held: `client` (API calls) and `resource` (ORM-style access); `self.region` is `session.region_name`. +- `clean()` extends `BaseCloud.clean()` to tear down `created_vpcs` and `created_keys` alongside tracked instances/images. +- Image IDs are AWS AMI IDs; image lookups query the Ubuntu public AMI catalog filtered by release/arch/`ImageType`/`include_deprecated` (see `ec2/cloud.py`). \ No newline at end of file diff --git a/pycloudlib/gce/.kb/gce.md b/pycloudlib/gce/.kb/gce.md new file mode 100644 index 00000000..8e13bcef --- /dev/null +++ b/pycloudlib/gce/.kb/gce.md @@ -0,0 +1,23 @@ +# Preface + +GCE backend specifics for `pycloudlib.gce`. Read before editing `gce/`; for config keys, constructor kwargs, and method behavior read `gce/cloud.py`, `gce/instance.py`, and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`GCE(BaseCloud)` authenticates via Google service-account/application-default credentials and the `google-cloud-compute` v1 SDK; `GceInstance(BaseInstance)` wraps a compute instance. GCE supports `ImageType` and resolves the project from config/env/`gcloud` CLI as a fallback. + + +# Important + +- Credentials resolve: explicit arg > `$GOOGLE_APPLICATION_CREDENTIALS` > config `credentials_path`. Project resolves: explicit arg > config > `$GOOGLE_CLOUD_PROJECT` > `gcloud config get-value project` (runs the CLI; failure raises `CloudSetupError`). See `gce/cloud.py` and `pycloudlib.toml.template` `[gce]` for keys. +- `gce/errors.py` defines GCE-specific exceptions (inheriting `PycloudlibException`); `gce/util.py` holds `get_credentials`/`raise_on_error` and the `gcloud` fallback. +- mypy: `google.*` is in `ignore_missing_imports` overrides; `pycloudlib.gce.cloud`/`util` have `check_untyped_defs = false` (TODO overrides in `pyproject.toml`) — prefer fixing typing over widening. + + +# Architecture + +- Several v1 clients are constructed (`ImagesClient`/`DisksClient`/`InstancesClient`/operations clients); `self.zone` is the full `{region}-{zone}` string. Snapshot/launch operations poll via the operations clients; the `google.cloud` logger is silenced to reduce noise. +- `clean()` extends `BaseCloud.clean()` for any GCE-specific tracked resources. \ No newline at end of file diff --git a/pycloudlib/ibm/.kb/ibm.md b/pycloudlib/ibm/.kb/ibm.md new file mode 100644 index 00000000..d422c448 --- /dev/null +++ b/pycloudlib/ibm/.kb/ibm.md @@ -0,0 +1,24 @@ +# Preface + +IBM VPC backend specifics for `pycloudlib.ibm`. Read before editing `ibm/`; for config keys, constructor kwargs, and method behavior read `ibm/cloud.py`, `ibm/instance.py`, and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`IBM(BaseCloud)` authenticates to IBM Cloud VPC via an IAM API key (`ibm-vpc` SDK + `ibm_platform_services` for resource groups); `IBMInstance(BaseInstance)` wraps a VPC instance. IBM supports custom VPC creation/reuse and a resource-group model. + + +# Important + +- **The `ibm-vpc` API version is pinned** in `ibm/cloud.py` (`VpcV1(..., version=...)` plus a `set_service_url` per region), and the SDK upper bound is constrained in `pyproject.toml` (`ibm-vpc >= 0.10, < 0.29.0`). When updating: check the SDK releases and update **both** the version string and the `<` upper bound together. +- `resource_group_id` and `vpc` are lazy properties (looked up on first access); a missing resource group raises `IBMException`. See `ibm/cloud.py` for the `from_existing`/`from_default` VPC selection. +- `ibm/_util.py` provides the iteration/wait helpers used across the backend; `ibm/errors.py` defines `IBMException`. +- mypy: `ibm_vpc.*`/`ibm_cloud_sdk_core.*`/`ibm_platform_services.*` are in `ignore_missing_imports`; `pycloudlib.ibm.instance` has `check_untyped_defs = false` (TODO overrides in `pyproject.toml`). + + +# Architecture + +- `IAMAuthenticator(api_key)` authenticates both `VpcV1` and `ResourceManagerV2`. The `VPC` helper (in `ibm/instance.py`) pairs the IBM VPC resource with the resolved resource-group id, region, and zone; floating IPs are selected by `floating_ip_substring` when provided. +- `clean()` extends `BaseCloud.clean()` to tear down `created_vpcs`/`created_keys`. \ No newline at end of file diff --git a/pycloudlib/ibm_classic/.kb/ibm-classic.md b/pycloudlib/ibm_classic/.kb/ibm-classic.md new file mode 100644 index 00000000..dc98542a --- /dev/null +++ b/pycloudlib/ibm_classic/.kb/ibm-classic.md @@ -0,0 +1,24 @@ +# Preface + +IBM Classic backend specifics for `pycloudlib.ibm_classic` (distinct from the IBM VPC backend `pycloudlib.ibm`). Read before editing `ibm_classic/`; for config keys and method behavior read `ibm_classic/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`IBMClassic(BaseCloud)` uses the SoftLayer SDK to manage classic (pre-VPC) IBM Cloud virtual servers; `IBMClassicInstance(BaseInstance)` wraps a VSManager virtual guest. This backend has no daily images and uses `globalIdentifier` for image references. + + +# Important + +- **No daily images**: `daily_image` delegates to `released_image` (see `ibm_classic/cloud.py`). +- **`delete_image` expects the integer image ID, not the `globalIdentifier`** returned by `released_image` — translating between the two is the caller's responsibility; a non-int raises `IBMClassicException`. This is a non-obvious footgun unique to this backend. +- Auth requires `username` + `api_key`; missing credentials raise `IBMClassicException`. See `pycloudlib.toml.template` `[ibm_classic]` for keys. +- `ibm_classic/errors.py` defines `IBMClassicException`; mypy: `Softlayer.*` is in `ignore_missing_imports` (`pyproject.toml`). + + +# Architecture + +- A single SoftLayer client is built from env and wrapped by four managers (`VSManager`/`ImageManager`/`SshKeyManager`/`NetworkManager`); `domain_name` is used to construct instance FQDNs. +- `clean()` extends `BaseCloud.clean()` to tear down `created_keys`/`created_security_groups`. \ No newline at end of file diff --git a/pycloudlib/lxd/.kb/lxd.md b/pycloudlib/lxd/.kb/lxd.md new file mode 100644 index 00000000..b017d136 --- /dev/null +++ b/pycloudlib/lxd/.kb/lxd.md @@ -0,0 +1,24 @@ +# Preface + +LXD backend specifics for `pycloudlib.lxd`. Read before editing `lxd/`; for class structure and method behavior read `lxd/cloud.py`, `lxd/instance.py`, `lxd/_images.py`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +LXD is a **local** backend (containers and VMs, not a public cloud) exposed as three classes: `LXDContainer` (preferred) and `LXDVirtualMachine` share a `_BaseLXD(BaseCloud)`; `LXD` is a deprecated alias of `LXDContainer`. Operations shell out to the `lxc` CLI via `pycloudlib.util.subp` — there is no SDK. + + +# Important + +- `LXD` emits a deprecation warning ("use `LXDContainer` instead"); use `LXDContainer`/`LXDVirtualMachine` in new code. +- Ensure the LXD daemon is initialized (`lxd init`) and the user has access. The `[lxd]` config section in `pycloudlib.toml.template` has no required keys. +- mypy: LXD modules are NOT in the relaxed-typing TODO overrides; keep them typed. + + +# Architecture + +- All cloud operations are `subp(["lxc", ...])` calls, often parsing `--format yaml` output via `yaml.safe_load`; there is no remote API client. Image discovery (fingerprints/serials, honoring `ImageType`) lives in `lxd/_images.py`; profile defaults in `lxd/defaults.py`. +- `LXDInstance`/`LXDVirtualMachineInstance` expose the standard `BaseInstance` `execute`/`run`/`Result` surface via `lxc exec`/`lxc file` (not paramiko) — see `.kb/ssh-and-instances.md` for the cross-cloud contract. `get_instance` returns the configured `_lxd_instance_cls`, enabling the container-vs-VM split. +- `clean()` extends `BaseCloud.clean()` to delete `created_snapshots`/`created_profiles`, tolerating "not found" errors. \ No newline at end of file diff --git a/pycloudlib/oci/.kb/oci.md b/pycloudlib/oci/.kb/oci.md new file mode 100644 index 00000000..07f08aa1 --- /dev/null +++ b/pycloudlib/oci/.kb/oci.md @@ -0,0 +1,24 @@ +# Preface + +OCI (Oracle Cloud Infrastructure) backend specifics for `pycloudlib.oci`. Read before editing `oci/`; for config keys and constructor kwargs read `oci/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`OCI(BaseCloud)` authenticates via the `oci` python SDK reading pycloudlib.toml oci.confg_path value or falling back to `~/.oci/config` (CLI or SDK initialized), a config dict, or env vars; `OciInstance(BaseInstance)` wraps a compute instance. OCI uses availability domains, compartments, and VCNs. + + +# Important + +- The OCI CLI must be initialized first (see the launch doc referenced in `oci/cloud.py`'s docstring). `compartment_id` falls back to `oci iam compartment get` via the CLI; failure raises `CloudSetupError`. +- Auth precedence: `config_dict` (validated via `oci.config.validate_config`) > env vars (`parse_oci_config_from_env_vars`) > `config_path` (default `~/.oci/config`). Passing `profile` alongside `config_dict` logs a warning and ignores the profile. +- `oci/utils.py` provides subnet lookup and `wait_till_ready`; `vcn_name` selects a VCN by exact name, else the newest VCN in the compartment is used. +- mypy: `oci.*` is in `ignore_missing_imports` (`pyproject.toml`); the module has `# pylint: disable=E1101` due to dynamic OCI SDK attributes. + + +# Architecture + +- The resolved OCI config dict (`self.oci_config`) drives all SDK clients; `get_instance` builds an `OciInstance` with the resolved network/subnet id. +- `clean()` extends `BaseCloud.clean()` for any OCI-specific tracked resources. diff --git a/pycloudlib/openstack/.kb/openstack.md b/pycloudlib/openstack/.kb/openstack.md new file mode 100644 index 00000000..93799730 --- /dev/null +++ b/pycloudlib/openstack/.kb/openstack.md @@ -0,0 +1,24 @@ +# Preface + +Openstack backend specifics for `pycloudlib.openstack`. Read before editing `openstack/`; for config keys and constructor kwargs read `openstack/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`Openstack(BaseCloud)` authenticates via `openstacksdk` (`openstack.connect()`), relying on pre-configured `OS_*` env vars or `clouds.yaml`; `OpenstackInstance(BaseInstance)` wraps a compute instance. Openstack is a private-cloud backend with no canonical Ubuntu image catalog. + + +# Important + +- `released_image`/`daily_image` raise `PycloudlibError` and `image_serial` raises `NotImplementedError` because Openstack deployments have no guaranteed Ubuntu image catalog. Cloud-agnostic code must tolerate this (see `examples/base_api.py`, which falls back from `released_image` to `daily_image` on `NotImplementedError`). +- Auth is delegated entirely to openstacksdk (`OS_*` env vars or `clouds.yaml` — see the openstacksdk config docs); only `network` is required in pycloudlib config. +- **SDK version is constrained** in `pyproject.toml` (`openstacksdk >= 1.1.0, < 1.5.0`, `python-openstackclient >= 5.2.1`); do not bump without testing. +- mypy: openstacksdk is imported but not in `ignore_missing_imports`; if typing breaks, add `openstack.*` to the overrides rather than relaxing the module. `openstack/errors.py` defines `OpenStackFlavorNotFound` (inheriting `PycloudlibException`). + + +# Architecture + +- `self.conn = openstack.connect()` performs the connection at init; network name → id resolution lives in `_get_network_id()` (network-not-found → `NetworkNotFoundError`). +- `clean()` extends `BaseCloud.clean()` for any Openstack-specific tracked resources. \ No newline at end of file diff --git a/pycloudlib/qemu/.kb/qemu.md b/pycloudlib/qemu/.kb/qemu.md new file mode 100644 index 00000000..904bd391 --- /dev/null +++ b/pycloudlib/qemu/.kb/qemu.md @@ -0,0 +1,23 @@ +# Preface + +QEMU backend specifics for `pycloudlib.qemu`. Read before editing `qemu/`; for config keys and constructor kwargs read `qemu/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`Qemu(BaseCloud)` manages **local** QEMU VMs from cloud images (no cloud account); `QemuInstance(BaseInstance)` controls a running VM via the QMP socket (`qemu.qmp`). It requires `qemu-system-x86_64`, `qemu-img`, and `genisoimage` on PATH and uses local directories for images and working files. + + +# Important + +- Missing prerequisites (`qemu-system-x86_64`/`qemu-img`/`genisoimage` on PATH) raise `MissingPrerequisiteError` at init with the Ubuntu apt hint. `image_dir` must be a valid existing path (`ValueError` otherwise). +- mypy: handled via `qemu.qmp`; `pycloudlib.qemu` is NOT in the relaxed-typing TODO overrides — keep it typed. + + +# Architecture + +- QEMU uses no remote cloud API: `launch` builds a disk from `image_dir`, attaches a cloud-init ISO (genisoimage), starts `qemu-system-x86_64` with port-forwarded SSH, and returns a `QemuInstance` connected via QMP. A per-session `parent_dir` (`working_dir / pycl-qemu-{tag}`) holds all artifacts; `qemu/util.py` provides `get_free_port`. +- Despite the local transport, `QemuInstance` exposes the standard `BaseInstance` SSH/exec/`Result` surface (SSH over the forwarded port) so cloud-agnostic callers work unchanged — see `.kb/ssh-and-instances.md`. +- `clean()` extends `BaseCloud.clean()` to remove the per-session `parent_dir` artifacts. \ No newline at end of file diff --git a/pycloudlib/vmware/.kb/vmware.md b/pycloudlib/vmware/.kb/vmware.md new file mode 100644 index 00000000..85de2b72 --- /dev/null +++ b/pycloudlib/vmware/.kb/vmware.md @@ -0,0 +1,23 @@ +# Preface + +VMWare backend specifics for `pycloudlib.vmware`. Read before editing `vmware/`; for config keys and constructor kwargs read `vmware/cloud.py` and `pycloudlib.toml.template`. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +`VMWare(BaseCloud)` shells out to `govc` (the govmomi CLI, not an SDK) against a vSphere endpoint; `VMWareInstance(BaseInstance)` wraps a VM. Image selection maps Ubuntu series to pre-uploaded VM templates. + + +# Important + +- Config mirrors the `GOVC_*` env vars (see `pycloudlib.toml.template` `[vmware]`). Missing `govc` on PATH raises a plain `ValueError` (not a pycloudlib exception) — a known inconsistency to preserve when editing. +- **`delete_image` refuses to delete core templates** (names in `SERIES_TO_TEMPLATE.values()` raise `ValueError`) and tolerates "not found" errors. `daily_image` delegates to `released_image` ("relying on whatever has been created/uploaded"). +- mypy: VMWare modules are NOT in the relaxed-typing TODO overrides; keep them typed. There is no `vmware/errors.py` (errors reuse the root `pycloudlib.errors` hierarchy). + + +# Architecture + +- All cloud operations are `govc` CLI invocations via `subprocess.run` with a constructed `GOVC_*` env dict; there is no Python SDK dependency. `folder` is the vSphere folder for both new VMs and template lookup. +- `clean()` extends `BaseCloud.clean()` for any VMWare-specific tracked resources, with the caveat that core templates are never deleted. \ No newline at end of file diff --git a/tests/.kb/testing.md b/tests/.kb/testing.md new file mode 100644 index 00000000..84c807a0 --- /dev/null +++ b/tests/.kb/testing.md @@ -0,0 +1,24 @@ +# Preface + +pytest markers, CI environments, and how to run unit vs integration tests for pycloudlib. Read before running tests; for marker/env specifics read `pyproject.toml` and `tox.ini` directly. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Overview + +Unit tests (`tests/unit_tests/`) are hermetic and mocked, run on every PR across Python 3.8/3.10/3.12. Integration tests (`tests/integration_tests/`) exercise real cloud APIs, require credentials, and are marker-gated; they run in dedicated tox envs, not the default `tox`. + + +# Important + +- Default `tox` envlist is `ruff, mypy, py38` — it does NOT run integration tests. Use `make test` (= `uv run tox`) for the CI-equivalent check. +- Unit tests run with `--doctest-modules`, so docstring doctests in `pycloudlib/` are executed too — keep them hermetic (no network, no real cloud calls). A failing doctest fails the unit test env. +- `integration-tests-main-check` exists because forked PRs can't access GH secrets: cloud-based tests run post-merge on main instead. Prefer the `ci` marker for new integration tests that should gate PRs. +- When adding a cloud, add `tests/unit_tests//` (mocked SDK, runs in CI) and optionally `tests/integration_tests//` (live, marker-gated). + + +# Architecture + +- Unit tests use `pytest-mock`/`mock` to patch SDK clients so no network calls occur; the `mock_ssh_keys` marker centralizes SSH-key mocking across the suite. +- pytest markers and `testpaths` live in `pyproject.toml`; integration tox envs and their flags live in `tox.ini`. CI badges reference `.github/workflows/ci.yaml`; the marker split lets one workflow run different subsets in PR vs post-merge contexts. \ No newline at end of file diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..6c60753a --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,16 @@ +# Preface + +The `tests/` tree: unit tests run in CI, integration tests need live cloud credentials. Read before adding or running tests. + +Read the top-level `.kb/agents.md` file before continuing below. + + +# Directory + +- `unit_tests/` - Mocked, hermetic tests run in CI via `tox` (`py38`/`py310`/`py312`). Mirrors the `pycloudlib/` layout with a subdir per cloud plus root-level modules. +- `integration_tests/` - Live cloud tests requiring real credentials. Gated by pytest markers; not run by default `tox`. Subdirs: `ec2/`, `gce/`, `ibm/`, `oracle/`. + + +# Documents + +- `.kb/testing.md` - pytest markers, CI environments, and how to run unit vs integration tests.