Metadata-Version: 2.4
Name: simplelxc
Version: 0.1.0
Summary: Basic LXC container automation for software isolation.
Project-URL: Homepage, https://git.sr.ht/~gk/simplelxc
Project-URL: Repository, https://git.sr.ht/~gk/simplelxc
Project-URL: Changelog, https://git.sr.ht/~gk/simplelxc/blob/main/CHANGELOG.md
Author-email: Graham King <grking.email@gmail.com>
License: MIT
License-File: LICENSE
Keywords: containers,lxc,lxd,orchestration,virtualization
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Operating System Kernels :: Linux
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Requires-Dist: pylxd>=2.3.6
Description-Content-Type: text/markdown

# `simplelxc`


Easily setup LXC containers for automated testing and interactive use of headless or terminal applications in isolated environments.

## Installation

```bash
pip install simplelxc
```
```bash
# or with uv
uv add simplelxc
```

## Requirements

- __Linux only__
- Python 3.10+
- LXD installed and initialised on your system

## Quick Start

SimpleLXC provides a simple API for automated container management:

```python
from simplelxc import Container

# Create a container
container = Container(
    name_prefix="myapp",
    base_image="images:archlinux",
    install_packages=["python", "git"],
)

# Start the container
container.start()

# Use it
container.execute("python --version")
container.copy_to("./myproject", "~/project")
container.execute("cd ~/project && python main.py")
container.execute_script("scripts/setup_db.sh", args="--force")

# Stop and delete it
container.stop()
```

Or load container configuration from a file:

```python
from simplelxc import Container

container = Container.from_file("container.json")
container.start()
container.execute("python --version")
...
```

### Context Manager

You can use the container as a context manager to ensure it is stopped and cleaned up automatically, even if errors occur:

```python
with Container(name_prefix="myapp") as c:
    c.execute("echo 'Doing work...'")
# Container stops and deletes automatically here
```


### Lifecycle Hooks

Containers expose a small set of lifecycle hooks that run at
well-defined points while a container is being created, provisioned,
and shut down. Hooks are configured on the ``Container.hooks``
attribute and are simple callables.

Core hooks:

- `before_start(plan)`
  - Called before any container is created or started.
  - ``plan`` is a small object with:
    - ``container_name``
    - ``image_name``
    - ``version``
    - ``will_use_cached_image``
    - ``will_create_cached_image``
    - ``auto_delete``
- `after_start(container)`
  - Called right after the container has been created and started,
    before any provisioning.
- `before_provision(container)` / `after_provision(container)`
  - Called around environment provisioning (package installation and
    setup scripts) when building a new environment (i.e. when no
    suitable cached image exists).
- `before_image_creation(container)` / `after_image_creation(container)`
  - Called when creating a cached LXD image after provisioning, if
    versioning/caching is enabled.
- `container_ready(container)`
  - Called once the container is fully usable:
    - For cached runs: after ``after_start``.
    - For provisioned runs: after ``after_provision`` and any
      image-creation hooks.
- `before_stop(container)` / `after_stop(container)`
  - Called before and after the container is explicitly stopped.
- `before_delete(container)` / `after_delete(container)`
  - Called before and after the container is deleted, when deletion
    occurs (for example, when ``auto_delete=True``).

Example:

```python
from simplelxc import Container

container = Container.from_file("container.json")

def log_plan(plan):
    print(f"Will create {plan.container_name} from {plan.image_name}")

def check_created(container):
    container.execute("echo 'Container created!'")

def check_provisioned(container):
    container.execute("echo 'Provisioning complete!'")

def save_logs(container):
    container.copy_from("~/logs", "./logs")

def on_ready(container):
    container.execute("echo 'Ready!'")

# Register hooks
container.hooks.before_start = log_plan
container.hooks.after_start = check_created
container.hooks.after_provision = check_provisioned
container.hooks.before_stop = save_logs
container.hooks.container_ready = on_ready

container.start()
```

### Configuration Files

Define container configuration in JSON files for easy management and version control:

**container.json:**
```json
{
  "container": {
    "name_prefix": "myapp",
    "auto_delete": true,
    "version": "1.0"
  },
  "image": {
    "base_image": "images:archlinux",
    "install_packages": ["python", "nodejs", "git"]
  }
}
```

**Python code:**
```python
from simplelxc import Container

# Load and start
container = Container.from_file("container.json")
container.start()

# Use it
container.execute("python --version")
```

This keeps static configuration in JSON files while keeping dynamic behavior (hooks) in Python code.

### Image Caching

SimpleLXC automatically caches container images based on version strings, significantly speeding up subsequent container creations:

```python
from simplelxc import Container

# First run: sets up environment and caches image
container = Container(
    name_prefix="myapp",
    base_image="images:archlinux",
    install_packages=["python", "git"],
    version="1.0",
)
container.start()

# Subsequent runs with same version: uses cached image (much faster!)
container2 = Container(
    name_prefix="myapp",
    base_image="images:archlinux",
    install_packages=["python", "git"],
    version="1.0",
)
container2.start()
```

## System Management

SimpleLXC provides top-level utilities for inspecting and cleaning up LXD resources system-wide:

```python
import simplelxc

# List all containers
print(simplelxc.get_container_names())

# List all local images
print(simplelxc.get_image_names())

# Check an image version
version = simplelxc.get_image_version("my-image")
if version:
    print(f"Image version: {version}")

# Delete all containers starting with "test-env"
count = simplelxc.prune_containers("test-env")
print(f"Deleted {count} containers")
```

## License


MIT License - see LICENSE file for details.

## Credits

SimpleLXC is built on top of [pylxd](https://github.com/lxc/pylxd), the Python library for LXD.

# API Documentation

## Classes

### `Container`

```python
class Container(
    name_prefix: str,
    base_image: str = 'images:archlinux',
    install_packages: list[str] | None = None,
    auto_delete: bool = True,
    version: str | None = None,
    kwargs: Any
) -> None
```

Automated container management.

This class is the main entry point for creating and managing a single container
instance. You can configure a container programmatically or load the
configuration from a JSON file.

The lifecycle (start, stop, provision) is managed automatically. You can hook
into various stages of the lifecycle using the `hooks` attribute.

**Example**

```python
from simplelxc import Container

# Create and start a container
c = Container(name_prefix="myapp", base_image="images:archlinux")
c.start()
c.execute("echo Hello World")
c.stop()
```

Initialize a container configuration.

This prepares the configuration but does not create the container yet.
Call `start()` to actually create and start the container.

The `name_prefix` is used to generate a unique container name.
If `auto_delete` is True (default), the container will be deleted when
`stop()` is called. `version` is used for caching the container image
to speed up subsequent starts.

#### Properties

##### `cwd: str`

Get or set the current working directory for commands executed in this
container.

##### `env: dict[str, str]`

Access the environment variable dictionary.

Modifying this dictionary will affect all subsequent `execute` calls.

##### `handle: ContainerHandle | None`

Get the container handle if running.

##### `hooks: LifecycleHooks`

Access lifecycle hooks for this container.

##### `name: str | None`

Get the container name, if started.

##### `snapshots: list[str]`

Get a list of all snapshot names for this container.

##### `version: str | None`

Get the container version.

#### Methods

##### `copy_from()`

```python
def copy_from(container_path: str, host_path: str) -> None
```

Copy a file or directory from the container to the host.

##### `copy_to()`

```python
def copy_to(host_path: str, container_path: str) -> None
```

Copy a file or directory from the host to the container.

##### `create_file()`

```python
def create_file(container_path: str, content: str) -> None
```

Create a file inside the container with the given string content.

##### `create_image()`

```python
def create_image(image_name: str, version: str | None = None) -> None
```

Save the current container state as a new local image.

This allows you to use this state as a base for other containers.

##### `delete()`

```python
def delete() -> None
```

Delete the container immediately.

This forcefully stops the container if it is running and then deletes it.

##### `delete_snapshot()`

```python
def delete_snapshot(name: str) -> None
```

Delete a snapshot.

##### `exec_replace()`

```python
def exec_replace(command: str, cwd: str | None = None, environment: dict | None = None) -> None
```

Execute command and replace current process (for interactive sessions).

This uses the 'lxc' CLI to replace the current process with the
command running in the container. Useful for interactive sessions.
This function does not return - it replaces the current process!

##### `execute()`

```python
def execute(command: str, cwd: str | None = None, environment: dict | None = None) -> CommandResult
```

Execute a shell command inside the container.

The command is executed via the container's shell. You can provide a
working directory `cwd` and a dictionary of `environment` variables.

Returns a `CommandResult` object containing the exit code, stdout, and stderr.

##### `execute_script()`

```python
def execute_script(local_path: str, args: str = '') -> CommandResult
```

Copy a script from the host and execute it inside the container.

The script is copied to the container, marked executable, run, and then deleted.
Use `args` to pass arguments to the script.

##### `exists()`

```python
def exists() -> bool
```

Check if the container exists.

##### `from_dict()`

```python
def from_dict(config: dict[str, Any]) -> Container
```

Create a Container instance from a dictionary configuration.

##### `from_file()`

```python
def from_file(config_path: str | Path) -> Container
```

Create a Container instance by loading configuration from a JSON file.

##### `install_package()`

```python
def install_package(package_name: str) -> None
```

Install a single package.

See `install_packages` for details on supported package managers and behavior.

##### `install_packages()`

```python
def install_packages(packages: list[str]) -> None
```

Install a list of packages using the container's package manager.

This method detects the operating system's package manager and installs
the requested packages.

1. Checks for package manager binaries in the container.
2. Automatically updates the package database before installation.
3. Installs the packages in non-interactive mode.

Supported Package Managers (default):
-   `apt-get` (Debian, Ubuntu, etc.)
-   `pacman` (Arch Linux, Manjaro)
-   `apk` (Alpine, Chimera)
-   `dnf` (Fedora, RHEL 8+, CentOS 8+)
-   `yum` (RHEL 7, CentOS 7)
-   `zypper` (OpenSUSE, SLES)
-   `microdnf` (UBI, minimal Fedora)
-   `xbps` (Void Linux)
-   `emerge` (Gentoo)

You can add support for other package managers using
`Container.register_package_manager()`.

For example:

```python
container.install_packages(["git", "python"])
```

##### `map_port()`

```python
def map_port(container_port: int, host_port: int | None = None) -> int
```

Expose a TCP port from the container to the host.

If `host_port` is not specified, a random free port on the host will be
assigned. Returns the port number on the host.

##### `realpath()`

```python
def realpath(pathname: str) -> str
```

Resolve the given pathname to an absolute pathname.

This will return the absolute path inside the container, specifically:
* Expand `~` to the containers home directory.
* Resolve absolute paths relative to the containers current working directory.

##### `register_package_manager()`

```python
def register_package_manager(manager: PackageManager) -> None
```

Register a new package manager or update an existing one.

This allows adding support for new Linux distributions or overriding
default behavior for existing ones.

##### `restart()`

```python
def restart(wait_for_network: bool = True) -> None
```

Restart the container.

If `wait_for_network` is True, this method blocks until the container
has network connectivity again.

##### `restore()`

```python
def restore(name: str) -> None
```

Restore the container to a previously saved snapshot state.

##### `snapshot()`

```python
def snapshot(name: str) -> None
```

Create a named snapshot of the current container state.

##### `start()`

```python
def start(kwargs: Any) -> Container
```

Start the container lifecycle.

This will create the container, start it, provision it (install packages,
run scripts), and execute any configured lifecycle hooks. If a cached image
matching the version exists, it will be used to speed up creation.

Configuration overrides can be passed as keyword arguments (e.g. `base_image`).
These overrides apply only to this start attempt.

Returns `self` to allow method chaining.

##### `stop()`

```python
def stop() -> None
```

Stop the container and perform cleanup.

If `auto_delete` was set to True (default), the container will be deleted
after stopping. Lifecycle hooks for stopping and deleting will be executed.

##### `wait_for()`

```python
def wait_for(condition: Callable[[], bool], timeout: int = 30, check_interval: float = 0.5) -> bool
```

Wait until the provided `condition` function returns True.

Returns True if the condition was met before the timeout, False otherwise.

##### `wait_for_command()`

```python
def wait_for_command(command: str, timeout: int = 30, check_interval: float = 0.5) -> bool
```

Wait until a command returns exit code 0.

Useful for polling until a service is ready.
Returns True if the command succeeded before the timeout.

##### `wait_for_http()`

```python
def wait_for_http(
    url: str,
    expected_status: int = 200,
    timeout: int = 30,
    check_interval: float = 0.5
) -> bool
```

Wait for a HTTP GET request to return the expected status code.

Uses `curl` inside the container.

##### `wait_for_network()`

```python
def wait_for_network(timeout: int = 10) -> None
```

Wait for container to have network connectivity.

##### `wait_for_port()`

```python
def wait_for_port(port: int, host: str = 'localhost', timeout: int = 30) -> bool
```

Wait until a TCP port is listening inside the container.

Returns True if the port is open before the timeout.

### `PackageManager`

```python
class PackageManager(
    name: str,
    install_cmd: str,
    update_cmd: str | None = None,
    binary: str | None = None
) -> None
```

Configuration for a system package manager.

Initialize self.  See help(type(self)) for accurate signature.

#### Methods

##### `get_binary()`

```python
def get_binary() -> str
```

### `CommandResult`

```python
class CommandResult(returncode: int, stdout: str, stderr: str) -> None
```

Result from executing a command in a container.

Initialize self.  See help(type(self)) for accurate signature.

#### Properties

##### `failed: bool`

Check if command failed (returncode != 0).

##### `success: bool`

Check if command succeeded (returncode == 0).

### `LifecycleHooks`

```python
class LifecycleHooks(
    before_start: Callable[[StartPlan], None] | None = None,
    after_start: Callable[[Container], None] | None = None,
    before_provision: Callable[[Container], None] | None = None,
    after_provision: Callable[[Container], None] | None = None,
    before_image_creation: Callable[[Container], None] | None = None,
    after_image_creation: Callable[[Container], None] | None = None,
    container_ready: Callable[[Container], None] | None = None,
    before_stop: Callable[[Container], None] | None = None,
    after_stop: Callable[[Container], None] | None = None,
    before_delete: Callable[[Container], None] | None = None,
    after_delete: Callable[[Container], None] | None = None
) -> None
```

Lifecycle callbacks for customizing container orchestration.

Hooks allow you to inject custom logic at specific points in the container's
lifecycle (creation, provisioning, shutdown). You can assign callables (functions
or lambdas) to these attributes on the `Container.hooks` object.

Unless otherwise noted, hooks receive the `Container` instance as their only
argument.

**Attributes**

- `before_start`: 
  Called before the container is created or started.
  Receives a `StartPlan` object describing the intended configuration.
  Useful for logging plans or validating preconditions.

- `after_start`: 
  Called immediately after the container is created and started, but before
  any environment provisioning (packages/scripts) begins.
  Useful for quick health checks or waiting for base services.

- `before_provision`: 
  Called before environment provisioning begins. Only runs if a new image
  is being built (i.e., not using a cached image).
  Useful for preparing files or state required by setup scripts.

- `after_provision`: 
  Called after all packages are installed and setup scripts have finished.
  Only runs if a new image was built.
  Useful for running migrations or validating the provisioned environment.

- `before_image_creation`: 
  Called before the provisioned container is saved as a cached image.
  Only runs if versioning is enabled and a new image is being built.
  Useful for cleaning up temporary files before baking the image.

- `after_image_creation`: 
  Called after the cached image has been successfully created.
  Only runs if versioning is enabled and a new image was built.

- `container_ready`: 
  Called when the container is fully ready for use.
  - If using a cached image: runs after `after_start`.
  - If provisioning: runs after `after_provision` (and image creation).
  This is the ideal place for final readiness checks.

- `before_stop`: 
  Called immediately before the container is stopped.
  Useful for capturing logs, artifacts, or saving state.

- `after_stop`: 
  Called after the container has been stopped.

- `before_delete`: 
  Called before the container is deleted.
  Only runs if `auto_delete` is True or `delete()` is called explicitly.

- `after_delete`: 
  Called after the container has been successfully deleted.


Initialize self.  See help(type(self)) for accurate signature.

### `ContainerError`

```python
class ContainerError()
```

Raised when container operations fail.

Initialize self.  See help(type(self)) for accurate signature.


---

# Module `simplelxc.system`

Global system-wide container and image management operations.

## Functions

### `system.delete_image()`

```python
def delete_image(image_name: str) -> None
```

Delete a local image by its alias.

### `system.get_container_names()`

```python
def get_container_names() -> list[str]
```

Get a list of names of all LXD containers on the system.

### `system.get_image_names()`

```python
def get_image_names() -> list[str]
```

Get a list of names (aliases) of all local LXD images.

### `system.get_image_version()`

```python
def get_image_version(image_name: str) -> str | None
```

Get the version string of a local image, if it exists.

Returns None if the image does not exist or has no version metadata.

### `system.image_exists()`

```python
def image_exists(image_name: str) -> bool
```

Check if a local image exists with the given alias.

### `system.prune_containers()`

```python
def prune_containers(prefix: str) -> int
```

Delete all containers with names starting with the given `prefix`.

Returns the number of containers deleted.

### `system.prune_images()`

```python
def prune_images(prefix: str) -> int
```

Delete all images with names starting with the given `prefix`.

Returns the number of images deleted.


---
