microheart 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajay Kumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ microheart
2
+ Copyright (c) 2026 Ajay Kumar. Licensed under the MIT License (see LICENSE).
3
+
4
+ This project downloads (or, in platform wheels, bundles) unmodified third-party software:
5
+
6
+ Firecracker - Apache License 2.0
7
+ Copyright Amazon.com, Inc. or its affiliates.
8
+ https://github.com/firecracker-microvm/firecracker
9
+
10
+ Linux guest kernel images from the Firecracker CI artifacts - GPL-2.0
11
+ Source: https://github.com/firecracker-microvm/firecracker
12
+ and https://www.kernel.org/
13
+
14
+ "Firecracker" is a name of Amazon Web Services. microheart is an independent project.
15
+ It is not affiliated with, endorsed by, or sponsored by AWS.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: microheart
3
+ Version: 0.1.0
4
+ Summary: Boot Firecracker microVMs from Python or the command line. Zero dependencies, Linux/KVM, Docker only to build the disk.
5
+ Author: Ajay Kumar
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Aj7Ay/microheart
8
+ Project-URL: Source, https://github.com/Aj7Ay/microheart
9
+ Project-URL: Issues, https://github.com/Aj7Ay/microheart/issues
10
+ Project-URL: Changelog, https://github.com/Aj7Ay/microheart/blob/main/CHANGELOG.md
11
+ Keywords: firecracker,microvm,kvm,virtualization,sandbox,vm,isolation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: System :: Emulators
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: NOTICE
27
+ Dynamic: license-file
28
+
29
+ # microheart
30
+
31
+ **Boot a [Firecracker](https://firecracker-microvm.github.io/) microVM with one command or three lines of Python.**
32
+ No Python dependencies. Linux with KVM only.
33
+
34
+ > **Unofficial. Not affiliated with or endorsed by Amazon Web Services.** "Firecracker" is an AWS project and name.
35
+ >
36
+ > **Status: alpha.** The code has not been tested on a real host yet. Test it before you rely on it.
37
+ > See [SECURITY.md](SECURITY.md) for the known limits.
38
+
39
+ ## What it does
40
+
41
+ Firecracker is a very small virtual machine monitor (3.6 MB). It starts a Linux guest in about a second,
42
+ but you must do many manual steps first: get the binary, get a kernel, build a disk, make a network
43
+ device, set NAT, write the API calls. microheart does these steps for you.
44
+
45
+ | Step | By hand | With microheart |
46
+ |---|---|---|
47
+ | Firecracker binary | download, unpack | `fetch` (or bundled in the wheel) |
48
+ | Guest kernel | find on S3, download | `fetch` (or bundled in the wheel) |
49
+ | Root disk | Docker image, export, `mkfs.ext4` | `build-rootfs` |
50
+ | Network | `ip tuntap`, `iptables`, forwarding | `up` |
51
+ | Boot | 5 curl calls to the API socket | `up` (one `--config-file`) |
52
+ | Login | key, route, DNS inside the guest | key, route and DNS are in the disk |
53
+ | Clean up | remove TAP and NAT rule | `down` |
54
+
55
+ ## Requirements
56
+
57
+ **To run a VM**
58
+
59
+ - Linux, `x86_64` (or `aarch64`), with `/dev/kvm` readable and writable by root
60
+ - Root access (`sudo`): TAP devices and `iptables` need it
61
+ - `ip`, `iptables`, `sysctl`, `ssh` (normally present)
62
+ - Python 3.9 or newer
63
+ - A virtualization-capable host. A cloud VM works if nested virtualization is on (tested idea: DigitalOcean droplet)
64
+
65
+ **To build the root disk (once, on any machine)**
66
+
67
+ - Docker, `mkfs.ext4`, `e2fsck`, `ssh-keygen`
68
+
69
+ Docker is **not** needed to run the VM. Build the disk once and copy the `.ext4` file.
70
+
71
+ ## Install
72
+
73
+ pip install microheart
74
+
75
+ ## Quick start
76
+
77
+ sudo microheart fetch # Firecracker + kernel (skipped when bundled in the wheel)
78
+ sudo microheart build-rootfs # build alpine.ext4 + an SSH key (needs Docker)
79
+ sudo microheart up # boot the VM and wait for SSH
80
+ sudo microheart ssh uname -a # run one command, or open a shell without arguments
81
+ sudo microheart down # stop the VM, remove tap0 and the NAT rule
82
+
83
+ Files go to `~/.cache/microheart/` (change with `MICROHEART_HOME`). With `sudo`, `~` is root's home.
84
+
85
+ ## Command reference
86
+
87
+ | Command | What it does |
88
+ |---|---|
89
+ | `microheart fetch` | Get the Firecracker binary and the newest CI kernel into the cache. |
90
+ | `microheart build-rootfs [--size 512M] [--output FILE]` | Build the ext4 disk from the bundled `Dockerfile`. Make an ed25519 key if none exists. |
91
+ | `microheart up` | Make `tap0`, set NAT, write the config file, start Firecracker, wait for SSH. |
92
+ | `microheart ssh [command...]` | SSH to the guest at `172.16.0.2` with the generated key. |
93
+ | `microheart status` | Print `running (pid N)` or `stopped`. |
94
+ | `microheart console [-n 40]` | Show the last lines of the guest console. |
95
+ | `microheart down` | Stop the VM and remove the TAP device and NAT rule. |
96
+
97
+ Global options (before the command): `--rootfs FILE`, `--key FILE`, `--vcpu N` (default 1), `--mem MIB` (default 256).
98
+
99
+ ## Python API
100
+
101
+ from microheart import MicroVM
102
+
103
+ with MicroVM("alpine.ext4", "vm_key", vcpu=2, mem_mib=512) as vm:
104
+ print(vm.ssh("uname -a").stdout)
105
+
106
+ | Member | Meaning |
107
+ |---|---|
108
+ | `MicroVM(rootfs, key, vcpu=1, mem_mib=256, tap="tap0", run_dir=None)` | Describe a VM. Nothing starts yet. |
109
+ | `.start(wait=60)` | Boot. Return the seconds until SSH worked. Needs root. |
110
+ | `.stop()` | Ask the guest to reboot (Firecracker then exits). Kill it after 10 s. |
111
+ | `.clean()` | `stop()` plus remove the TAP device and NAT rule. |
112
+ | `.ssh(cmd, check=True, timeout=30)` | Run a command in the guest. Return `subprocess.CompletedProcess`. |
113
+ | `.running()`, `.pid()` | State of the Firecracker process. |
114
+ | `.machine_config()` | Read `/machine-config` from the Firecracker API. |
115
+ | `.tail(n)` | Last `n` console lines. |
116
+
117
+ ## How it works
118
+
119
+ 1. `assets.py` finds the Firecracker binary and kernel (bundled, then cache, then download).
120
+ 2. `net.py` makes `tap0` with host IP `172.16.0.1/30`, turns on IP forwarding and adds `MASQUERADE`.
121
+ 3. `vm.py` writes `vm_config.json` and starts `firecracker --config-file ... --enable-pci`.
122
+ 4. The guest MAC `06:00:AC:10:00:02` and the disk's `/etc/network/interfaces` give the guest `172.16.0.2/30`
123
+ with gateway `172.16.0.1`. DNS (`8.8.8.8`) is written at boot.
124
+ 5. microheart polls SSH until the guest answers.
125
+
126
+ Network layout:
127
+
128
+ guest eth0 172.16.0.2/30 <--virtio--> host tap0 172.16.0.1/30 --NAT--> host default interface --> internet
129
+
130
+ One VM at a time for now: the IP addresses and `tap0` are fixed.
131
+
132
+ ## Root disk recipe
133
+
134
+ `src/microheart/data/` holds the recipe:
135
+
136
+ - `Dockerfile`: Alpine 3.20, OpenRC, OpenSSH, iproute2, curl. SSH is key-only.
137
+ - `files/`: network config, DNS at boot, serial console options.
138
+ - `build-rootfs.sh`: Docker image to folder, add your public key, `mkfs.ext4 -d`, `e2fsck`.
139
+
140
+ Edit the recipe to add your own packages, then run `build-rootfs` again.
141
+
142
+ ## Security notes
143
+
144
+ - The serial console logs in as **root with no password** (test setting).
145
+ Edit `data/files/etc/conf.d/agetty.ttyS0` before real use.
146
+ - SSH accepts keys only. The key is made on your machine and is never part of the package.
147
+ - The guest can reach the internet through host NAT. The guest and host `FORWARD` policy is set to `ACCEPT`.
148
+ - Firecracker's `jailer` (extra isolation) is not used yet.
149
+ - microheart runs as root and changes host network settings. Read the code first.
150
+
151
+ ## Limits and roadmap
152
+
153
+ - Alpha, untested on a real host.
154
+ - One VM at a time (fixed addresses).
155
+ - No snapshots, no jailer, no multi-VM networking.
156
+ - Planned: per-VM addresses, `--config-file` options, snapshots, jailer support, `aarch64` tests.
157
+
158
+ ## Development
159
+
160
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Build a wheel with the binary and kernel inside:
161
+
162
+ python scripts/bundle.py && python -m build --wheel
163
+ wheel tags --platform-tag manylinux_2_17_x86_64 --remove dist/*.whl
164
+
165
+ ## License
166
+
167
+ MIT, see [LICENSE](LICENSE). Third-party software (Firecracker: Apache-2.0; Linux kernel: GPL-2.0) is
168
+ downloaded or bundled unchanged. See [NOTICE](NOTICE).
@@ -0,0 +1,140 @@
1
+ # microheart
2
+
3
+ **Boot a [Firecracker](https://firecracker-microvm.github.io/) microVM with one command or three lines of Python.**
4
+ No Python dependencies. Linux with KVM only.
5
+
6
+ > **Unofficial. Not affiliated with or endorsed by Amazon Web Services.** "Firecracker" is an AWS project and name.
7
+ >
8
+ > **Status: alpha.** The code has not been tested on a real host yet. Test it before you rely on it.
9
+ > See [SECURITY.md](SECURITY.md) for the known limits.
10
+
11
+ ## What it does
12
+
13
+ Firecracker is a very small virtual machine monitor (3.6 MB). It starts a Linux guest in about a second,
14
+ but you must do many manual steps first: get the binary, get a kernel, build a disk, make a network
15
+ device, set NAT, write the API calls. microheart does these steps for you.
16
+
17
+ | Step | By hand | With microheart |
18
+ |---|---|---|
19
+ | Firecracker binary | download, unpack | `fetch` (or bundled in the wheel) |
20
+ | Guest kernel | find on S3, download | `fetch` (or bundled in the wheel) |
21
+ | Root disk | Docker image, export, `mkfs.ext4` | `build-rootfs` |
22
+ | Network | `ip tuntap`, `iptables`, forwarding | `up` |
23
+ | Boot | 5 curl calls to the API socket | `up` (one `--config-file`) |
24
+ | Login | key, route, DNS inside the guest | key, route and DNS are in the disk |
25
+ | Clean up | remove TAP and NAT rule | `down` |
26
+
27
+ ## Requirements
28
+
29
+ **To run a VM**
30
+
31
+ - Linux, `x86_64` (or `aarch64`), with `/dev/kvm` readable and writable by root
32
+ - Root access (`sudo`): TAP devices and `iptables` need it
33
+ - `ip`, `iptables`, `sysctl`, `ssh` (normally present)
34
+ - Python 3.9 or newer
35
+ - A virtualization-capable host. A cloud VM works if nested virtualization is on (tested idea: DigitalOcean droplet)
36
+
37
+ **To build the root disk (once, on any machine)**
38
+
39
+ - Docker, `mkfs.ext4`, `e2fsck`, `ssh-keygen`
40
+
41
+ Docker is **not** needed to run the VM. Build the disk once and copy the `.ext4` file.
42
+
43
+ ## Install
44
+
45
+ pip install microheart
46
+
47
+ ## Quick start
48
+
49
+ sudo microheart fetch # Firecracker + kernel (skipped when bundled in the wheel)
50
+ sudo microheart build-rootfs # build alpine.ext4 + an SSH key (needs Docker)
51
+ sudo microheart up # boot the VM and wait for SSH
52
+ sudo microheart ssh uname -a # run one command, or open a shell without arguments
53
+ sudo microheart down # stop the VM, remove tap0 and the NAT rule
54
+
55
+ Files go to `~/.cache/microheart/` (change with `MICROHEART_HOME`). With `sudo`, `~` is root's home.
56
+
57
+ ## Command reference
58
+
59
+ | Command | What it does |
60
+ |---|---|
61
+ | `microheart fetch` | Get the Firecracker binary and the newest CI kernel into the cache. |
62
+ | `microheart build-rootfs [--size 512M] [--output FILE]` | Build the ext4 disk from the bundled `Dockerfile`. Make an ed25519 key if none exists. |
63
+ | `microheart up` | Make `tap0`, set NAT, write the config file, start Firecracker, wait for SSH. |
64
+ | `microheart ssh [command...]` | SSH to the guest at `172.16.0.2` with the generated key. |
65
+ | `microheart status` | Print `running (pid N)` or `stopped`. |
66
+ | `microheart console [-n 40]` | Show the last lines of the guest console. |
67
+ | `microheart down` | Stop the VM and remove the TAP device and NAT rule. |
68
+
69
+ Global options (before the command): `--rootfs FILE`, `--key FILE`, `--vcpu N` (default 1), `--mem MIB` (default 256).
70
+
71
+ ## Python API
72
+
73
+ from microheart import MicroVM
74
+
75
+ with MicroVM("alpine.ext4", "vm_key", vcpu=2, mem_mib=512) as vm:
76
+ print(vm.ssh("uname -a").stdout)
77
+
78
+ | Member | Meaning |
79
+ |---|---|
80
+ | `MicroVM(rootfs, key, vcpu=1, mem_mib=256, tap="tap0", run_dir=None)` | Describe a VM. Nothing starts yet. |
81
+ | `.start(wait=60)` | Boot. Return the seconds until SSH worked. Needs root. |
82
+ | `.stop()` | Ask the guest to reboot (Firecracker then exits). Kill it after 10 s. |
83
+ | `.clean()` | `stop()` plus remove the TAP device and NAT rule. |
84
+ | `.ssh(cmd, check=True, timeout=30)` | Run a command in the guest. Return `subprocess.CompletedProcess`. |
85
+ | `.running()`, `.pid()` | State of the Firecracker process. |
86
+ | `.machine_config()` | Read `/machine-config` from the Firecracker API. |
87
+ | `.tail(n)` | Last `n` console lines. |
88
+
89
+ ## How it works
90
+
91
+ 1. `assets.py` finds the Firecracker binary and kernel (bundled, then cache, then download).
92
+ 2. `net.py` makes `tap0` with host IP `172.16.0.1/30`, turns on IP forwarding and adds `MASQUERADE`.
93
+ 3. `vm.py` writes `vm_config.json` and starts `firecracker --config-file ... --enable-pci`.
94
+ 4. The guest MAC `06:00:AC:10:00:02` and the disk's `/etc/network/interfaces` give the guest `172.16.0.2/30`
95
+ with gateway `172.16.0.1`. DNS (`8.8.8.8`) is written at boot.
96
+ 5. microheart polls SSH until the guest answers.
97
+
98
+ Network layout:
99
+
100
+ guest eth0 172.16.0.2/30 <--virtio--> host tap0 172.16.0.1/30 --NAT--> host default interface --> internet
101
+
102
+ One VM at a time for now: the IP addresses and `tap0` are fixed.
103
+
104
+ ## Root disk recipe
105
+
106
+ `src/microheart/data/` holds the recipe:
107
+
108
+ - `Dockerfile`: Alpine 3.20, OpenRC, OpenSSH, iproute2, curl. SSH is key-only.
109
+ - `files/`: network config, DNS at boot, serial console options.
110
+ - `build-rootfs.sh`: Docker image to folder, add your public key, `mkfs.ext4 -d`, `e2fsck`.
111
+
112
+ Edit the recipe to add your own packages, then run `build-rootfs` again.
113
+
114
+ ## Security notes
115
+
116
+ - The serial console logs in as **root with no password** (test setting).
117
+ Edit `data/files/etc/conf.d/agetty.ttyS0` before real use.
118
+ - SSH accepts keys only. The key is made on your machine and is never part of the package.
119
+ - The guest can reach the internet through host NAT. The guest and host `FORWARD` policy is set to `ACCEPT`.
120
+ - Firecracker's `jailer` (extra isolation) is not used yet.
121
+ - microheart runs as root and changes host network settings. Read the code first.
122
+
123
+ ## Limits and roadmap
124
+
125
+ - Alpha, untested on a real host.
126
+ - One VM at a time (fixed addresses).
127
+ - No snapshots, no jailer, no multi-VM networking.
128
+ - Planned: per-VM addresses, `--config-file` options, snapshots, jailer support, `aarch64` tests.
129
+
130
+ ## Development
131
+
132
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Build a wheel with the binary and kernel inside:
133
+
134
+ python scripts/bundle.py && python -m build --wheel
135
+ wheel tags --platform-tag manylinux_2_17_x86_64 --remove dist/*.whl
136
+
137
+ ## License
138
+
139
+ MIT, see [LICENSE](LICENSE). Third-party software (Firecracker: Apache-2.0; Linux kernel: GPL-2.0) is
140
+ downloaded or bundled unchanged. See [NOTICE](NOTICE).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "microheart"
7
+ version = "0.1.0"
8
+ description = "Boot Firecracker microVMs from Python or the command line. Zero dependencies, Linux/KVM, Docker only to build the disk."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Ajay Kumar"}]
13
+ keywords = ["firecracker", "microvm", "kvm", "virtualization", "sandbox", "vm", "isolation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: System :: Emulators",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Aj7Ay/microheart"
31
+ Source = "https://github.com/Aj7Ay/microheart"
32
+ Issues = "https://github.com/Aj7Ay/microheart/issues"
33
+ Changelog = "https://github.com/Aj7Ay/microheart/blob/main/CHANGELOG.md"
34
+
35
+ [project.scripts]
36
+ microheart = "microheart.cli:main"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ microheart = ["data/*", "data/files/**/*", "bin/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """microheart: run Firecracker microVMs with no Python dependencies."""
2
+ from .vm import MicroVM
3
+
4
+ __all__ = ["MicroVM"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,31 @@
1
+ """Tiny client for the Firecracker API on a Unix socket (standard library only)."""
2
+ from __future__ import annotations
3
+
4
+ import http.client
5
+ import json
6
+ import socket
7
+
8
+
9
+ class _UnixConn(http.client.HTTPConnection):
10
+ def __init__(self, path: str, timeout: float = 5):
11
+ super().__init__("localhost", timeout=timeout)
12
+ self._path = path
13
+
14
+ def connect(self):
15
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
16
+ self.sock.settimeout(self.timeout)
17
+ self.sock.connect(self._path)
18
+
19
+
20
+ def request(sock_path: str, method: str, route: str, body: dict | None = None):
21
+ conn = _UnixConn(sock_path)
22
+ try:
23
+ data = json.dumps(body) if body is not None else None
24
+ conn.request(method, route, body=data, headers={"Content-Type": "application/json"})
25
+ resp = conn.getresponse()
26
+ raw = resp.read().decode()
27
+ if resp.status >= 300:
28
+ raise RuntimeError(f"{method} {route} -> {resp.status}: {raw}")
29
+ return json.loads(raw) if raw else None
30
+ finally:
31
+ conn.close()
@@ -0,0 +1,84 @@
1
+ """Find the Firecracker binary and the guest kernel.
2
+
3
+ Order: files bundled in the wheel (microheart/bin), then the cache folder.
4
+ If neither exists, download them into the cache folder.
5
+ """
6
+ import os
7
+ import platform
8
+ import re
9
+ import shutil
10
+ import stat
11
+ import tarfile
12
+ import tempfile
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ S3 = "https://s3.amazonaws.com/spec.ccfc.min"
17
+ RELEASES = "https://github.com/firecracker-microvm/firecracker/releases"
18
+ PKG = Path(__file__).parent
19
+
20
+
21
+ def cache_dir() -> Path:
22
+ base = os.environ.get("MICROHEART_HOME") or os.path.join(
23
+ os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "microheart")
24
+ p = Path(base)
25
+ p.mkdir(parents=True, exist_ok=True)
26
+ return p
27
+
28
+
29
+ def _get(url: str) -> bytes:
30
+ with urllib.request.urlopen(url, timeout=60) as r:
31
+ return r.read()
32
+
33
+
34
+ def _save(url: str, dest: Path) -> None:
35
+ with urllib.request.urlopen(url, timeout=120) as r, open(dest, "wb") as f:
36
+ shutil.copyfileobj(r, f)
37
+
38
+
39
+ def _find(name_glob: str):
40
+ for d in (PKG / "bin", cache_dir()):
41
+ hits = sorted(d.glob(name_glob)) if d.is_dir() else []
42
+ if hits:
43
+ return hits[-1]
44
+ return None
45
+
46
+
47
+ def firecracker_path() -> Path:
48
+ found = _find("firecracker")
49
+ if found:
50
+ return found
51
+ arch = platform.machine()
52
+ latest_url = urllib.request.urlopen(f"{RELEASES}/latest", timeout=60).geturl()
53
+ tag = latest_url.rstrip("/").split("/")[-1]
54
+ dest = cache_dir() / "firecracker"
55
+ with tempfile.TemporaryDirectory() as tmp:
56
+ tgz = Path(tmp) / "fc.tgz"
57
+ _save(f"{RELEASES}/download/{tag}/firecracker-{tag}-{arch}.tgz", tgz)
58
+ with tarfile.open(tgz) as t:
59
+ t.extractall(tmp, filter="data") if hasattr(tarfile, "data_filter") else t.extractall(tmp)
60
+ src = Path(tmp) / f"release-{tag}-{arch}" / f"firecracker-{tag}-{arch}"
61
+ shutil.copy(src, dest)
62
+ dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
63
+ return dest
64
+
65
+
66
+ def kernel_path() -> Path:
67
+ found = _find("vmlinux-*")
68
+ if found:
69
+ return found
70
+ arch = platform.machine()
71
+ top = _get(f"{S3}?list-type=2&prefix=firecracker-ci/&delimiter=/").decode()
72
+ prefixes = re.findall(r"<Prefix>(firecracker-ci/[0-9]{8}-[^/]+/)</Prefix>", top)
73
+ prefix = sorted(prefixes)[-1]
74
+ listing = _get(f"{S3}?list-type=2&prefix={prefix}{arch}/vmlinux-").decode()
75
+ keys = re.findall(rf"<Key>({re.escape(prefix)}{arch}/vmlinux-\d+\.\d+\.\d{{1,3}})</Key>", listing)
76
+ key = sorted(keys, key=lambda k: [int(x) for x in re.findall(r"\d+", k.rsplit("vmlinux-", 1)[1])])[-1]
77
+ dest = cache_dir() / key.rsplit("/", 1)[1]
78
+ _save(f"{S3}/{key}", dest)
79
+ return dest
80
+
81
+
82
+ def recipe_dir() -> Path:
83
+ """Folder with the Dockerfile and build-rootfs.sh."""
84
+ return PKG / "data"
@@ -0,0 +1,85 @@
1
+ """Command line: microheart build-rootfs | fetch | up | ssh | status | console | down"""
2
+ import argparse
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import assets
9
+ from .vm import MicroVM
10
+
11
+
12
+ def _paths():
13
+ home = assets.cache_dir()
14
+ return home / "alpine.ext4", home / "vm_key"
15
+
16
+
17
+ def _vm(a):
18
+ rootfs, key = _paths()
19
+ return MicroVM(a.rootfs or rootfs, a.key or key, vcpu=a.vcpu, mem_mib=a.mem)
20
+
21
+
22
+ def cmd_fetch(a):
23
+ print("firecracker:", assets.firecracker_path())
24
+ print("kernel: ", assets.kernel_path())
25
+
26
+
27
+ def cmd_build(a):
28
+ rootfs, key = _paths()
29
+ if not key.exists():
30
+ subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-f", str(key), "-N", ""], check=True)
31
+ script = assets.recipe_dir() / "build-rootfs.sh"
32
+ subprocess.run(["bash", str(script), f"{key}.pub", a.size, str(a.output or rootfs)], check=True)
33
+
34
+
35
+ def cmd_up(a):
36
+ secs = _vm(a).start()
37
+ print(f"VM is up ({secs:.1f} s). Try: sudo microheart ssh")
38
+
39
+
40
+ def cmd_ssh(a):
41
+ vm = _vm(a)
42
+ os.execvp("ssh", ["ssh", "-i", vm.key, "-o", "StrictHostKeyChecking=no",
43
+ "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR",
44
+ "root@172.16.0.2", *a.command])
45
+
46
+
47
+ def cmd_status(a):
48
+ vm = _vm(a)
49
+ print(f"running (pid {vm.pid()})" if vm.running() else "stopped")
50
+
51
+
52
+ def cmd_console(a):
53
+ print(_vm(a).tail(a.lines))
54
+
55
+
56
+ def cmd_down(a):
57
+ _vm(a).clean()
58
+ print("stopped; removed tap device and NAT rule")
59
+
60
+
61
+ def main(argv=None):
62
+ p = argparse.ArgumentParser(prog="microheart")
63
+ p.add_argument("--rootfs"); p.add_argument("--key")
64
+ p.add_argument("--vcpu", type=int, default=1); p.add_argument("--mem", type=int, default=256)
65
+ sub = p.add_subparsers(dest="cmd", required=True)
66
+ sub.add_parser("fetch", help="download Firecracker and the kernel").set_defaults(f=cmd_fetch)
67
+ b = sub.add_parser("build-rootfs", help="build the ext4 disk from the Dockerfile (needs Docker, root)")
68
+ b.add_argument("--size", default="512M"); b.add_argument("--output", type=Path)
69
+ b.set_defaults(f=cmd_build)
70
+ sub.add_parser("up", help="boot the VM").set_defaults(f=cmd_up)
71
+ s = sub.add_parser("ssh", help="log in to the guest"); s.add_argument("command", nargs="*")
72
+ s.set_defaults(f=cmd_ssh)
73
+ sub.add_parser("status").set_defaults(f=cmd_status)
74
+ c = sub.add_parser("console"); c.add_argument("-n", "--lines", type=int, default=40)
75
+ c.set_defaults(f=cmd_console)
76
+ sub.add_parser("down", help="stop the VM and clean the network").set_defaults(f=cmd_down)
77
+ a = p.parse_args(argv)
78
+ try:
79
+ a.f(a)
80
+ except (PermissionError, RuntimeError, TimeoutError, subprocess.CalledProcessError) as e:
81
+ sys.exit(f"ERROR: {e}")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,43 @@
1
+ # Root file system for a Firecracker microVM.
2
+ # Base: Alpine Linux. Init system: OpenRC.
3
+ # This follows the recipe in docs/rootfs-and-kernel-setup.md (official Firecracker docs).
4
+ # Status: alpha, not yet tested on a real host.
5
+
6
+ FROM alpine:3.20
7
+
8
+ # Packages:
9
+ # openrc the init system (starts services at boot)
10
+ # util-linux gives the agetty program (serial console login)
11
+ # openssh the SSH server
12
+ # iproute2 the "ip" command
13
+ # curl, ca-certificates test tools for the network
14
+ RUN apk add --no-cache openrc util-linux openssh iproute2 curl ca-certificates
15
+
16
+ # Serial console login on ttyS0 (from the official doc).
17
+ RUN ln -s agetty /etc/init.d/agetty.ttyS0 \
18
+ && echo ttyS0 > /etc/securetty
19
+
20
+ # Config files: network, DNS at boot, console options.
21
+ COPY files/ /
22
+ RUN chmod 755 /etc/local.d/10-dns.start
23
+
24
+ # SSH: allow root login with a key only. No passwords.
25
+ RUN echo "PermitRootLogin prohibit-password" >> /etc/ssh/sshd_config \
26
+ && echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
27
+
28
+ # Services.
29
+ # boot level: special file systems, host name, root file system, network.
30
+ # default level: console login, SSH, local scripts (DNS).
31
+ RUN rc-update add devfs boot \
32
+ && rc-update add procfs boot \
33
+ && rc-update add sysfs boot \
34
+ && rc-update add hostname boot \
35
+ && rc-update add bootmisc boot \
36
+ && rc-update add root boot \
37
+ && rc-update add networking boot \
38
+ && rc-update add agetty.ttyS0 default \
39
+ && rc-update add sshd default \
40
+ && rc-update add local default
41
+
42
+ # No SSH key and no password go into this image.
43
+ # The build script adds the public key to the disk file after the build.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # Build an ext4 disk file from the Dockerfile in this folder.
3
+ #
4
+ # Usage: sudo ./build-rootfs.sh <public-key-file> [size] [output-file]
5
+ # Example: sudo ./build-rootfs.sh ~/.cache/microheart/vm_key.pub 512M alpine.ext4
6
+ #
7
+ # Steps:
8
+ # 1. Docker builds the image and writes its files into a folder.
9
+ # 2. The script adds the SSH public key and the host name to the folder.
10
+ # 3. mkfs.ext4 -d copies the folder into a new ext4 file.
11
+ # 4. e2fsck checks the new file.
12
+ set -euo pipefail
13
+
14
+ PUBKEY="${1:?Give the path of a public key file (ends in .pub)}"
15
+ SIZE="${2:-512M}"
16
+ OUT="${3:-alpine.ext4}"
17
+
18
+ [ -f "$PUBKEY" ] || { echo "ERROR: $PUBKEY does not exist"; exit 1; }
19
+ [ "$(id -u)" -eq 0 ] || { echo "ERROR: run this script as root"; exit 1; }
20
+
21
+ WORK="$(mktemp -d)"
22
+ trap 'rm -rf "$WORK"' EXIT
23
+
24
+ echo "Step 1: build the image and write its files to a folder"
25
+ # --output type=local writes the image files straight to a folder.
26
+ # It avoids the empty /etc/hostname, /etc/hosts and /etc/resolv.conf
27
+ # that "docker export" can leave behind. Verify this on your Docker version.
28
+ docker build --output "type=local,dest=$WORK/root" "$(dirname "$0")"
29
+
30
+ echo "Step 2: add the SSH public key and the host name"
31
+ install -d -m 700 "$WORK/root/root/.ssh"
32
+ install -m 600 "$PUBKEY" "$WORK/root/root/.ssh/authorized_keys"
33
+ echo "fc-alpine" > "$WORK/root/etc/hostname"
34
+ printf '127.0.0.1 localhost\n' > "$WORK/root/etc/hosts"
35
+ mkdir -p "$WORK/root"/{dev,proc,sys,run,tmp,var}
36
+
37
+ echo "Step 3: make the ext4 file ($SIZE)"
38
+ rm -f "$OUT"
39
+ truncate -s "$SIZE" "$OUT"
40
+ mkfs.ext4 -q -d "$WORK/root" -F "$OUT"
41
+
42
+ echo "Step 4: check the file system"
43
+ e2fsck -fn "$OUT" > /dev/null && echo "OK: $OUT is a valid ext4 file system"
44
+ ls -lh "$OUT"
@@ -0,0 +1,4 @@
1
+ # Serial console login. TEST ONLY: root logs in with no password.
2
+ # Anyone who reaches the console becomes root. Remove this for real use.
3
+ # If login does not work, run "passwd -d root" in the build script instead.
4
+ agetty_options="--autologin root --noclear"
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Runs at boot (OpenRC "local" service).
3
+ # Docker does not keep /etc/resolv.conf in an image, so we write it at boot.
4
+ printf 'nameserver 8.8.8.8\noptions single-request-reopen\n' > /etc/resolv.conf
@@ -0,0 +1,8 @@
1
+ auto lo
2
+ iface lo inet loopback
3
+
4
+ auto eth0
5
+ iface eth0 inet static
6
+ address 172.16.0.2
7
+ netmask 255.255.255.252
8
+ gateway 172.16.0.1
@@ -0,0 +1,38 @@
1
+ """Host network: one TAP device and one NAT rule. Needs root."""
2
+ import json
3
+ import subprocess
4
+
5
+
6
+ def _run(*cmd, check=True, capture=False):
7
+ return subprocess.run(cmd, check=check, text=True,
8
+ stdout=subprocess.PIPE if capture else None,
9
+ stderr=subprocess.DEVNULL if not check else None)
10
+
11
+
12
+ def host_iface() -> str:
13
+ out = _run("ip", "-j", "route", "list", "default", capture=True).stdout
14
+ return json.loads(out)[0]["dev"]
15
+
16
+
17
+ def up(tap: str, host_ip: str) -> str:
18
+ if _run("ip", "link", "show", tap, check=False).returncode != 0:
19
+ _run("ip", "tuntap", "add", "dev", tap, "mode", "tap")
20
+ if f"{host_ip}/30" not in _run("ip", "addr", "show", "dev", tap, capture=True).stdout:
21
+ _run("ip", "addr", "add", f"{host_ip}/30", "dev", tap)
22
+ _run("ip", "link", "set", "dev", tap, "up")
23
+ _run("sysctl", "-q", "-w", "net.ipv4.ip_forward=1")
24
+ _run("iptables", "-P", "FORWARD", "ACCEPT")
25
+ iface = host_iface()
26
+ if _run("iptables", "-t", "nat", "-C", "POSTROUTING", "-o", iface, "-j", "MASQUERADE",
27
+ check=False).returncode != 0:
28
+ _run("iptables", "-t", "nat", "-A", "POSTROUTING", "-o", iface, "-j", "MASQUERADE")
29
+ return iface
30
+
31
+
32
+ def down(tap: str) -> None:
33
+ _run("ip", "link", "del", tap, check=False)
34
+ try:
35
+ iface = host_iface()
36
+ except (IndexError, subprocess.CalledProcessError):
37
+ return
38
+ _run("iptables", "-t", "nat", "-D", "POSTROUTING", "-o", iface, "-j", "MASQUERADE", check=False)
@@ -0,0 +1,116 @@
1
+ """MicroVM: build a config file, boot Firecracker, reach the guest over SSH."""
2
+ import json
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from . import api, assets, net
10
+
11
+ GUEST_IP = "172.16.0.2" # fixed in the rootfs: data/files/etc/network/interfaces
12
+ HOST_IP = "172.16.0.1"
13
+ GUEST_MAC = "06:00:AC:10:00:02"
14
+
15
+
16
+ class MicroVM:
17
+ def __init__(self, rootfs, key, vcpu=1, mem_mib=256, tap="tap0", run_dir=None):
18
+ self.rootfs = str(Path(rootfs).resolve())
19
+ self.key = str(Path(key).resolve())
20
+ self.vcpu, self.mem, self.tap = vcpu, mem_mib, tap
21
+ self.run_dir = Path(run_dir or assets.cache_dir() / "run")
22
+ self.run_dir.mkdir(parents=True, exist_ok=True)
23
+ self.sock = str(self.run_dir / "firecracker.socket")
24
+ self.pidfile = self.run_dir / "firecracker.pid"
25
+ self.console = self.run_dir / "console.log"
26
+
27
+ # -- state ---------------------------------------------------------
28
+ def pid(self):
29
+ try:
30
+ pid = int(self.pidfile.read_text())
31
+ os.kill(pid, 0)
32
+ return pid
33
+ except (FileNotFoundError, ValueError, ProcessLookupError):
34
+ return None
35
+
36
+ def running(self) -> bool:
37
+ return self.pid() is not None
38
+
39
+ # -- lifecycle -----------------------------------------------------
40
+ def config(self) -> dict:
41
+ return {
42
+ "boot-source": {"kernel_image_path": str(assets.kernel_path()),
43
+ "boot_args": "console=ttyS0 reboot=k panic=1"},
44
+ "drives": [{"drive_id": "rootfs", "path_on_host": self.rootfs,
45
+ "is_root_device": True, "is_read_only": False}],
46
+ "network-interfaces": [{"iface_id": "net1", "guest_mac": GUEST_MAC,
47
+ "host_dev_name": self.tap}],
48
+ "machine-config": {"vcpu_count": self.vcpu, "mem_size_mib": self.mem},
49
+ }
50
+
51
+ def start(self, wait=60):
52
+ if os.geteuid() != 0:
53
+ raise PermissionError("run as root (use sudo)")
54
+ if not os.access("/dev/kvm", os.R_OK | os.W_OK):
55
+ raise PermissionError("no access to /dev/kvm")
56
+ if self.running():
57
+ return
58
+ fc = assets.firecracker_path()
59
+ net.up(self.tap, HOST_IP)
60
+ cfg = self.run_dir / "vm_config.json"
61
+ cfg.write_text(json.dumps(self.config(), indent=2))
62
+ Path(self.sock).unlink(missing_ok=True)
63
+ with open(self.console, "w") as log:
64
+ proc = subprocess.Popen([str(fc), "--api-sock", self.sock, "--config-file", str(cfg),
65
+ "--enable-pci"], stdout=log, stderr=subprocess.STDOUT,
66
+ stdin=subprocess.DEVNULL, start_new_session=True)
67
+ self.pidfile.write_text(str(proc.pid))
68
+ t0 = time.time()
69
+ while time.time() - t0 < wait:
70
+ if proc.poll() is not None:
71
+ raise RuntimeError("Firecracker stopped:\n" + self.tail())
72
+ if self.ssh("true", check=False).returncode == 0:
73
+ return time.time() - t0
74
+ time.sleep(1)
75
+ raise TimeoutError(f"no SSH after {wait} s:\n" + self.tail())
76
+
77
+ def stop(self):
78
+ pid = self.pid()
79
+ if pid:
80
+ self.ssh("reboot", check=False) # a guest reboot makes Firecracker exit
81
+ for _ in range(10):
82
+ if not self.running():
83
+ break
84
+ time.sleep(1)
85
+ if self.running():
86
+ os.kill(pid, signal.SIGTERM)
87
+ self.pidfile.unlink(missing_ok=True)
88
+ Path(self.sock).unlink(missing_ok=True)
89
+
90
+ def clean(self):
91
+ self.stop()
92
+ net.down(self.tap)
93
+
94
+ # -- guest access --------------------------------------------------
95
+ def ssh(self, command: str, check=True, timeout=30):
96
+ return subprocess.run(
97
+ ["ssh", "-i", self.key, "-o", "StrictHostKeyChecking=no",
98
+ "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR",
99
+ "-o", "ConnectTimeout=3", f"root@{GUEST_IP}", command],
100
+ check=check, text=True, capture_output=True, timeout=timeout)
101
+
102
+ def machine_config(self):
103
+ return api.request(self.sock, "GET", "/machine-config")
104
+
105
+ def tail(self, n=30) -> str:
106
+ try:
107
+ return "\n".join(self.console.read_text().splitlines()[-n:])
108
+ except FileNotFoundError:
109
+ return ""
110
+
111
+ def __enter__(self):
112
+ self.start()
113
+ return self
114
+
115
+ def __exit__(self, *exc):
116
+ self.clean()
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: microheart
3
+ Version: 0.1.0
4
+ Summary: Boot Firecracker microVMs from Python or the command line. Zero dependencies, Linux/KVM, Docker only to build the disk.
5
+ Author: Ajay Kumar
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Aj7Ay/microheart
8
+ Project-URL: Source, https://github.com/Aj7Ay/microheart
9
+ Project-URL: Issues, https://github.com/Aj7Ay/microheart/issues
10
+ Project-URL: Changelog, https://github.com/Aj7Ay/microheart/blob/main/CHANGELOG.md
11
+ Keywords: firecracker,microvm,kvm,virtualization,sandbox,vm,isolation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: System :: Emulators
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: NOTICE
27
+ Dynamic: license-file
28
+
29
+ # microheart
30
+
31
+ **Boot a [Firecracker](https://firecracker-microvm.github.io/) microVM with one command or three lines of Python.**
32
+ No Python dependencies. Linux with KVM only.
33
+
34
+ > **Unofficial. Not affiliated with or endorsed by Amazon Web Services.** "Firecracker" is an AWS project and name.
35
+ >
36
+ > **Status: alpha.** The code has not been tested on a real host yet. Test it before you rely on it.
37
+ > See [SECURITY.md](SECURITY.md) for the known limits.
38
+
39
+ ## What it does
40
+
41
+ Firecracker is a very small virtual machine monitor (3.6 MB). It starts a Linux guest in about a second,
42
+ but you must do many manual steps first: get the binary, get a kernel, build a disk, make a network
43
+ device, set NAT, write the API calls. microheart does these steps for you.
44
+
45
+ | Step | By hand | With microheart |
46
+ |---|---|---|
47
+ | Firecracker binary | download, unpack | `fetch` (or bundled in the wheel) |
48
+ | Guest kernel | find on S3, download | `fetch` (or bundled in the wheel) |
49
+ | Root disk | Docker image, export, `mkfs.ext4` | `build-rootfs` |
50
+ | Network | `ip tuntap`, `iptables`, forwarding | `up` |
51
+ | Boot | 5 curl calls to the API socket | `up` (one `--config-file`) |
52
+ | Login | key, route, DNS inside the guest | key, route and DNS are in the disk |
53
+ | Clean up | remove TAP and NAT rule | `down` |
54
+
55
+ ## Requirements
56
+
57
+ **To run a VM**
58
+
59
+ - Linux, `x86_64` (or `aarch64`), with `/dev/kvm` readable and writable by root
60
+ - Root access (`sudo`): TAP devices and `iptables` need it
61
+ - `ip`, `iptables`, `sysctl`, `ssh` (normally present)
62
+ - Python 3.9 or newer
63
+ - A virtualization-capable host. A cloud VM works if nested virtualization is on (tested idea: DigitalOcean droplet)
64
+
65
+ **To build the root disk (once, on any machine)**
66
+
67
+ - Docker, `mkfs.ext4`, `e2fsck`, `ssh-keygen`
68
+
69
+ Docker is **not** needed to run the VM. Build the disk once and copy the `.ext4` file.
70
+
71
+ ## Install
72
+
73
+ pip install microheart
74
+
75
+ ## Quick start
76
+
77
+ sudo microheart fetch # Firecracker + kernel (skipped when bundled in the wheel)
78
+ sudo microheart build-rootfs # build alpine.ext4 + an SSH key (needs Docker)
79
+ sudo microheart up # boot the VM and wait for SSH
80
+ sudo microheart ssh uname -a # run one command, or open a shell without arguments
81
+ sudo microheart down # stop the VM, remove tap0 and the NAT rule
82
+
83
+ Files go to `~/.cache/microheart/` (change with `MICROHEART_HOME`). With `sudo`, `~` is root's home.
84
+
85
+ ## Command reference
86
+
87
+ | Command | What it does |
88
+ |---|---|
89
+ | `microheart fetch` | Get the Firecracker binary and the newest CI kernel into the cache. |
90
+ | `microheart build-rootfs [--size 512M] [--output FILE]` | Build the ext4 disk from the bundled `Dockerfile`. Make an ed25519 key if none exists. |
91
+ | `microheart up` | Make `tap0`, set NAT, write the config file, start Firecracker, wait for SSH. |
92
+ | `microheart ssh [command...]` | SSH to the guest at `172.16.0.2` with the generated key. |
93
+ | `microheart status` | Print `running (pid N)` or `stopped`. |
94
+ | `microheart console [-n 40]` | Show the last lines of the guest console. |
95
+ | `microheart down` | Stop the VM and remove the TAP device and NAT rule. |
96
+
97
+ Global options (before the command): `--rootfs FILE`, `--key FILE`, `--vcpu N` (default 1), `--mem MIB` (default 256).
98
+
99
+ ## Python API
100
+
101
+ from microheart import MicroVM
102
+
103
+ with MicroVM("alpine.ext4", "vm_key", vcpu=2, mem_mib=512) as vm:
104
+ print(vm.ssh("uname -a").stdout)
105
+
106
+ | Member | Meaning |
107
+ |---|---|
108
+ | `MicroVM(rootfs, key, vcpu=1, mem_mib=256, tap="tap0", run_dir=None)` | Describe a VM. Nothing starts yet. |
109
+ | `.start(wait=60)` | Boot. Return the seconds until SSH worked. Needs root. |
110
+ | `.stop()` | Ask the guest to reboot (Firecracker then exits). Kill it after 10 s. |
111
+ | `.clean()` | `stop()` plus remove the TAP device and NAT rule. |
112
+ | `.ssh(cmd, check=True, timeout=30)` | Run a command in the guest. Return `subprocess.CompletedProcess`. |
113
+ | `.running()`, `.pid()` | State of the Firecracker process. |
114
+ | `.machine_config()` | Read `/machine-config` from the Firecracker API. |
115
+ | `.tail(n)` | Last `n` console lines. |
116
+
117
+ ## How it works
118
+
119
+ 1. `assets.py` finds the Firecracker binary and kernel (bundled, then cache, then download).
120
+ 2. `net.py` makes `tap0` with host IP `172.16.0.1/30`, turns on IP forwarding and adds `MASQUERADE`.
121
+ 3. `vm.py` writes `vm_config.json` and starts `firecracker --config-file ... --enable-pci`.
122
+ 4. The guest MAC `06:00:AC:10:00:02` and the disk's `/etc/network/interfaces` give the guest `172.16.0.2/30`
123
+ with gateway `172.16.0.1`. DNS (`8.8.8.8`) is written at boot.
124
+ 5. microheart polls SSH until the guest answers.
125
+
126
+ Network layout:
127
+
128
+ guest eth0 172.16.0.2/30 <--virtio--> host tap0 172.16.0.1/30 --NAT--> host default interface --> internet
129
+
130
+ One VM at a time for now: the IP addresses and `tap0` are fixed.
131
+
132
+ ## Root disk recipe
133
+
134
+ `src/microheart/data/` holds the recipe:
135
+
136
+ - `Dockerfile`: Alpine 3.20, OpenRC, OpenSSH, iproute2, curl. SSH is key-only.
137
+ - `files/`: network config, DNS at boot, serial console options.
138
+ - `build-rootfs.sh`: Docker image to folder, add your public key, `mkfs.ext4 -d`, `e2fsck`.
139
+
140
+ Edit the recipe to add your own packages, then run `build-rootfs` again.
141
+
142
+ ## Security notes
143
+
144
+ - The serial console logs in as **root with no password** (test setting).
145
+ Edit `data/files/etc/conf.d/agetty.ttyS0` before real use.
146
+ - SSH accepts keys only. The key is made on your machine and is never part of the package.
147
+ - The guest can reach the internet through host NAT. The guest and host `FORWARD` policy is set to `ACCEPT`.
148
+ - Firecracker's `jailer` (extra isolation) is not used yet.
149
+ - microheart runs as root and changes host network settings. Read the code first.
150
+
151
+ ## Limits and roadmap
152
+
153
+ - Alpha, untested on a real host.
154
+ - One VM at a time (fixed addresses).
155
+ - No snapshots, no jailer, no multi-VM networking.
156
+ - Planned: per-VM addresses, `--config-file` options, snapshots, jailer support, `aarch64` tests.
157
+
158
+ ## Development
159
+
160
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Build a wheel with the binary and kernel inside:
161
+
162
+ python scripts/bundle.py && python -m build --wheel
163
+ wheel tags --platform-tag manylinux_2_17_x86_64 --remove dist/*.whl
164
+
165
+ ## License
166
+
167
+ MIT, see [LICENSE](LICENSE). Third-party software (Firecracker: Apache-2.0; Linux kernel: GPL-2.0) is
168
+ downloaded or bundled unchanged. See [NOTICE](NOTICE).
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ NOTICE
3
+ README.md
4
+ pyproject.toml
5
+ src/microheart/__init__.py
6
+ src/microheart/api.py
7
+ src/microheart/assets.py
8
+ src/microheart/cli.py
9
+ src/microheart/net.py
10
+ src/microheart/vm.py
11
+ src/microheart.egg-info/PKG-INFO
12
+ src/microheart.egg-info/SOURCES.txt
13
+ src/microheart.egg-info/dependency_links.txt
14
+ src/microheart.egg-info/entry_points.txt
15
+ src/microheart.egg-info/top_level.txt
16
+ src/microheart/data/Dockerfile
17
+ src/microheart/data/build-rootfs.sh
18
+ src/microheart/data/files/etc/conf.d/agetty.ttyS0
19
+ src/microheart/data/files/etc/local.d/10-dns.start
20
+ src/microheart/data/files/etc/network/interfaces
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ microheart = microheart.cli:main
@@ -0,0 +1 @@
1
+ microheart