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.
planis a small object with:container_nameimage_nameversionwill_use_cached_imagewill_create_cached_imageauto_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_provisionand any image-creation hooks.
- For cached runs: after
- Called once the container is fully usable:
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).
- Called before and after the container is deleted, when deletion
occurs (for example, when
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.
- Checks for package manager binaries in the container.
- Automatically updates the package database before installation.
- 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 aStartPlanobject 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.
- If using a cached image: runs after
-
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 ifauto_deleteis True ordelete()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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
934eec2c552ca0d276f3811ee05058f9e4628a848d5d9497f1f34bdd34f50fe8
|
|
| MD5 |
6e3af165c8b306c87d87c7b72242d37c
|
|
| BLAKE2b-256 |
3a4bff3f868a3008e380743ccc37d49c26c8e56ef54a050205d0558dcc60b425
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b16ca896939cb791d73bfcee38bcb28c908cc9c03b804334568ce6581b062f42
|
|
| MD5 |
994e9794c05546e6691ae9ecff557587
|
|
| BLAKE2b-256 |
24c78351365174b67c4fe489da9b48518dbf33167877782caaf5d4cdf18006af
|