Skip to main content

multipass-sdk

Unofficial Python SDK for Canonical Multipass. Wraps the Multipass CLI to manage Ubuntu VMs programmatically.

  • Full coverage of the Multipass CLI (launch, list, find, exec, transfer, mount, snapshot, clone, …)
  • Testable without Multipass installed — inject a FakeBackend in unit tests
  • Typed exception hierarchy, dataclass models, no external runtime dependencies beyond haikunator and pyyaml

Installation

uv add git+https://github.com/Nanofaas/multipass-vm-sdk.git

To pin to a specific commit or tag:

uv add git+https://github.com/Nanofaas/multipass-vm-sdk.git@v0.6.0

Multipass itself must be installed on the machine where the SDK is used at runtime. It is not required for unit tests.


Quick start

from multipass import MultipassClient, VmConfig

client = MultipassClient()

# Launch a VM (name is auto-generated if omitted)
vm = client.launch(name="my-vm", cpus=2, memory="1G", disk="10G", image="22.04")

# Or use a VmConfig for reusable configs
vm = client.launch(VmConfig(name="my-vm", cpus=4, memory="8G"))

# Launch multiple VMs in parallel (rolls back on failure)
vms = client.launch_many(
    [
        VmConfig(name="web", cpus=2),
        VmConfig(name="db", cpus=4, memory="8G"),
    ]
)

# Wait until SSH is reachable, then connect
ip = vm.wait_ready(timeout=180, port=22)
print(f"VM ready at {ip}")

# Run a command
result = vm.exec(["uname", "-r"])
print(result.stdout)

# Structured exec with cwd and env
result = vm.exec_structured(
    ["sh", "-c", 'echo "$PWD $MSG"'], cwd="/tmp", env={"MSG": "hello"}
)

# Lifecycle
vm.stop()
vm.start()
vm.suspend()
vm.restart()

# Cleanup
vm.delete(purge=True)

API reference

MultipassClient

client = MultipassClient(cmd="multipass")  # cmd: path to the CLI binary
Method Description
launch(name, image, *, cpus, memory, disk, cloud_init, cloud_init_config) → MultipassVM Launch a new VM. Accepts str | VmConfig | None as first argument
launch_many(configs, *, max_workers) → list[MultipassVM] Launch multiple VMs in parallel; rolls back all on any failure
ensure_running(name, image, *, cpus, memory, disk, cloud_init, cloud_init_config) → MultipassVM Idempotent: launch, start, or no-op so the VM ends up Running
get_vm(name) → MultipassVM Get a VM object by name
list_vms() → list[VmInfo] List all VMs
find() → list[ImageInfo] List available images
purge() Permanently delete all soft-deleted VMs
networks() → list[NetworkInfo] List available networks
version() → VersionInfo Multipass and multipassd versions
get(key) → str Read a Multipass setting
set(key, value) Write a Multipass setting
aliases() → list[AliasInfo] List all aliases
alias(name, vm, command) Create an alias
unalias(name) Remove an alias

MultipassVM

Method Description
info() → VmInfo Get current VM state and metadata
start() Start the VM
stop(*, force, time) Stop the VM
restart() Restart the VM
suspend() Suspend the VM
recover() Recover a VM in error state
delete(*, purge) Delete the VM (soft or permanent)
exec(command: list[str]) → CommandResult Run a command in the VM
exec_structured(argv, *, env, cwd) → CommandResult Structured exec: builds the bash prologue (cd + export) from typed arguments
transfer(source, dest) Transfer files between host and VM (recursive)
mount(source, target, *, mount_type, uid_map, gid_map) Mount a host directory
unmount(mount) Unmount a directory
snapshots() → list[SnapshotInfo] List snapshots
snapshot(name, *, comment) → SnapshotInfo Create a snapshot (VM must be stopped)
restore(snapshot, *, destructive) Restore a snapshot
clone(new_name) → MultipassVM Clone the VM (VM must be stopped)
wait_for_ip(timeout, *, interval) → str Poll until the VM has an IPv4 address
wait_ready(timeout, port, *, interval) → str Poll until the VM has an IP and a TCP port is reachable

wait_for_ip / wait_ready

These methods make the common "launch → SSH" pattern reliable:

vm = client.launch(name="worker", disk="10G")

# Returns the first IPv4 address once assigned
ip = vm.wait_for_ip(timeout=120)

# Returns the IP once the given TCP port is reachable (default: 22)
ip = vm.wait_ready(timeout=180, port=22)

Both raise MultipassTimeoutError if the deadline is exceeded.

VmConfig

Reusable configuration for VM launches, shared across launch(), launch_many(), and ensure_running():

from multipass import VmConfig

cfg = VmConfig(name="worker", cpus=4, memory="8G", disk="30G", image="22.04")

vm = client.launch(cfg)
vms = client.launch_many([cfg, VmConfig(name="worker2", cpus=2)])

Fields: name, image, cpus (default 1), memory (default "1G"), disk (default "5G"), cloud_init, cloud_init_config.

CloudInitConfig

Structured cloud-init configuration as an alternative to raw dicts:

from multipass import CloudInitConfig

cfg = CloudInitConfig(
    packages=["git", "curl"],
    ssh_authorized_keys=["ssh-ed25519 AAAA..."],
    runcmd=[["apt-get", "update"], ["apt-get", "upgrade", "-y"]],
)
vm = client.launch(
    name="my-vm",
    cloud_init_config=cfg.to_dict(),
)

Fields: packages, ssh_authorized_keys, runcmd, write_files, users — all optional. to_dict() returns only the fields that were set.

launch_many

Launch multiple VMs concurrently. If any launch fails, already-created VMs are deleted:

configs = [
    VmConfig(name="web", cpus=2, memory="4G"),
    VmConfig(name="db", cpus=4, memory="16G", disk="100G"),
]
vms = client.launch_many(configs, max_workers=4)

File transfer

Use instance-name:/path notation for VM paths, plain paths for host paths:

vm.transfer("/host/file.txt", "my-vm:/home/ubuntu/file.txt")
vm.transfer("my-vm:/home/ubuntu/output.txt", "/host/dest/")

Snapshots

vm.stop()
snap = vm.snapshot("snap1", comment="before upgrade")
vm.start()

# restore (--destructive consumes the snapshot)
vm.stop()
vm.restore("snap1", destructive=True)
vm.start()

cloud-init

Pass a file path, a YAML string, or a dict — the SDK handles the rest.

File path (you manage the file):

vm = client.launch(name="my-vm", cloud_init="/home/user/cloud-init.yaml")

Inline dict (serialized to JSON, which cloud-init accepts natively):

vm = client.launch(
    name="my-vm",
    cloud_init_config={
        "packages": ["git", "curl"],
        "runcmd": ["apt-get upgrade -y"],
    },
)

Inline YAML string:

vm = client.launch(
    name="my-vm",
    cloud_init_config="""
#cloud-config
packages:
  - git
  - curl
runcmd:
  - apt-get upgrade -y
""",
)

When cloud_init_config is used, the SDK writes a temporary file to the user's home directory (not /tmp) so that Multipass installed via snap can read it. The file is deleted automatically after launch.

Custom user and SSH key

from pathlib import Path

ssh_key = Path("~/.ssh/id_rsa.pub").expanduser().read_text().strip()

vm = client.launch(
    name="my-vm",
    cloud_init_config={
        "users": [
            {
                "name": "michele",
                "groups": ["sudo"],
                "shell": "/bin/bash",
                "sudo": "ALL=(ALL) NOPASSWD:ALL",
                "ssh_authorized_keys": [ssh_key],
            }
        ]
    },
)

To keep the default ubuntu user alongside your custom one, add "default" as the first entry in the users list:

"users": ["default", {"name": "michele", ...}]

SSH key injection helper

find_ssh_public_key() returns the content of the first SSH public key found in ~/.ssh/ (priority: ed25519 → rsa → ecdsa → dsa), or None if none exists. Combine it with cloud_init_config to make new VMs immediately accessible:

from multipass import MultipassClient, find_ssh_public_key

client = MultipassClient()
pub_key = find_ssh_public_key()

vm = client.ensure_running(
    "my-vm",
    cloud_init_config={"ssh_authorized_keys": [pub_key]} if pub_key else None,
)
ip = vm.wait_ready(timeout=180)

Exceptions

All exceptions inherit from MultipassError.

Exception When raised
MultipassCommandError CLI exits with non-zero status
MultipassNotInstalledError multipass binary not found
MultipassTimeoutError wait_for_ip / wait_ready deadline exceeded
VmNotFoundError VM does not exist
VmAlreadyRunningError Operation requires stopped VM
VmNotRunningError Operation requires running VM
VmAlreadySuspendedError VM is already suspended

Testing without Multipass

Inject a FakeBackend to unit-test code that uses the SDK:

import json
from multipass import MultipassClient
from multipass.testing import FakeBackend
from multipass import CommandResult

backend = FakeBackend(
    {
        ("multipass", "list", "--format", "json"): CommandResult(
            args=[],
            returncode=0,
            stdout=json.dumps({"list": []}),
            stderr="",
        )
    }
)
client = MultipassClient(backend=backend)
vms = client.list_vms()  # no Multipass required

FakeBackend also supports queued responses for polling scenarios and tracks cwd/env for assertions:

backend = FakeBackend()
backend.push(
    "multipass",
    "info",
    "my-vm",
    "--format",
    "json",
    result=CommandResult(..., stdout=info_no_ip),
)
backend.push(
    "multipass",
    "info",
    "my-vm",
    "--format",
    "json",
    result=CommandResult(..., stdout=info_with_ip),
)

assert backend.last_cwd() is None
assert backend.last_env() is None

Running the tests

# Setup
uv sync
uv run pre-commit install

# Unit tests (no Multipass required)
uv run pytest tests/unit/ -v

# Unit tests with the coverage gate (fails below 90%)
uv run pytest tests/unit --cov --cov-report=term-missing

# Integration tests (require Multipass installed and running)
uv run pytest -m integration -v

# Lint and format (ruff)
uv run ruff check .
uv run ruff format .

# Type checking (basedpyright)
uv run basedpyright

# Security (bandit)
uv run bandit -c pyproject.toml -r src

# Everything pre-commit runs, the same way CI does
uv run pre-commit run --all-files

End-to-end script

The SDK ships a multipass-vm-e2e console script for full lifecycle verification:

uv run multipass-vm-e2e
uv run multipass-vm-e2e --name my-test --cpus 2 --memory 4G --disk 10G
uv run multipass-vm-e2e --count 3
uv run multipass-vm-e2e --configs '[{"name":"web","cpus":2},{"name":"db","cpus":4}]'
uv run multipass-vm-e2e --list-images
uv run multipass-vm-e2e --skip transfer,clone

Five-stage pipeline: launch → basic verification → feature tests (exec_structured, stop/start, restart, transfer, clone, snapshot/restore) → cleanup clones → delete VMs.

Feature tests run only in single-VM mode (--count 1). Use --skip all for a basic lifecycle-only test or --skip <feature>,... to skip individual tests.

The integration test suite covers: full VM lifecycle, resources, soft delete + purge, wait_for_ip, wait_ready + SSH, suspend/resume, file transfer, snapshot/restore, clone, cloud-init, and error handling.


Contributing

Send a pull request. Unit tests and pre-commit (ruff, basedpyright, bandit) must pass; integration tests are welcome but not required in CI.

Release files for multipass-vm-sdk 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for multipass-vm-sdk 0.6.0
File Size Uploaded
multipass_vm_sdk-0.6.0.tar.gz 67.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for multipass-vm-sdk 0.6.0
File Interpreter ABI Platform
multipass_vm_sdk-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 90.5 kB

Release files / multipass_vm_sdk-0.6.0.tar.gz

Download URL multipass_vm_sdk-0.6.0.tar.gz
Size 67.8 kB
Tags Source
SHA-256 checksum
How to use checksums
9195fa1d9a9aabf4adfce89ce2e07fef264485dd104afe5c04a633e76397eabd
BLAKE2b-256 checksum
How to use checksums
0fa4f1f7539c62ef55015b8ab1c1a043cce62f57ab023974156e9ced9d91d0bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / multipass_vm_sdk-0.6.0-py3-none-any.whl

Download URL multipass_vm_sdk-0.6.0-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0ba64a7ed4fccbcdef2456f27ca0d546a162d1017eb48d806c8c762e62664f34
BLAKE2b-256 checksum
How to use checksums
0f7f865c30de3878a8239d10e475102ea4d307f15060447f647053ac6b4392be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page