Skip to main content

Basic LXC container automation for software isolation.

Project description

simplelxc

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

Installation

pip install simplelxc
# 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:

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:

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:

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:

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:

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

Python code:

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:

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:

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, the Python library for LXD.

API Documentation

Classes

Container

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

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()
def copy_from(container_path: str, host_path: str) -> None

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

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

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

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

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

create_image()
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()
def delete() -> None

Delete the container immediately.

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

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

Delete a snapshot.

exec_replace()
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()
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()
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()
def exists() -> bool

Check if the container exists.

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

Create a Container instance from a dictionary configuration.

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

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

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

Install a single package.

See install_packages for details on supported package managers and behavior.

install_packages()
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:

container.install_packages(["git", "python"])
map_port()
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()
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()
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()
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()
def restore(name: str) -> None

Restore the container to a previously saved snapshot state.

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

Create a named snapshot of the current container state.

start()
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()
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()
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()
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()
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()
def wait_for_network(timeout: int = 10) -> None

Wait for container to have network connectivity.

wait_for_port()
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

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()
def get_binary() -> str

CommandResult

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

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

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()

def delete_image(image_name: str) -> None

Delete a local image by its alias.

system.get_container_names()

def get_container_names() -> list[str]

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

system.get_image_names()

def get_image_names() -> list[str]

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

system.get_image_version()

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()

def image_exists(image_name: str) -> bool

Check if a local image exists with the given alias.

system.prune_containers()

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()

def prune_images(prefix: str) -> int

Delete all images with names starting with the given prefix.

Returns the number of images deleted.


Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simplelxc-0.1.0.tar.gz (39.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simplelxc-0.1.0-py3-none-any.whl (32.6 kB view details)

Uploaded Python 3

File details

Details for the file simplelxc-0.1.0.tar.gz.

File metadata

  • Download URL: simplelxc-0.1.0.tar.gz
  • Upload date:
  • Size: 39.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for simplelxc-0.1.0.tar.gz
Algorithm Hash digest
SHA256 934eec2c552ca0d276f3811ee05058f9e4628a848d5d9497f1f34bdd34f50fe8
MD5 6e3af165c8b306c87d87c7b72242d37c
BLAKE2b-256 3a4bff3f868a3008e380743ccc37d49c26c8e56ef54a050205d0558dcc60b425

See more details on using hashes here.

File details

Details for the file simplelxc-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: simplelxc-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for simplelxc-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b16ca896939cb791d73bfcee38bcb28c908cc9c03b804334568ce6581b062f42
MD5 994e9794c05546e6691ae9ecff557587
BLAKE2b-256 24c78351365174b67c4fe489da9b48518dbf33167877782caaf5d4cdf18006af

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page