Initial commit
commit
6a31ac392a
|
@ -0,0 +1,9 @@
|
||||||
|
.mypy_cache/
|
||||||
|
/.venv
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
*.py[co]
|
||||||
|
/kernel.img
|
||||||
|
src/ocivm/kernel.img
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "kernel"]
|
||||||
|
path = kernel
|
||||||
|
url = https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
|
|
@ -0,0 +1,2 @@
|
||||||
|
include src/ocivm/kernel.img
|
||||||
|
include src/ocivm/linuxrc
|
|
@ -0,0 +1,42 @@
|
||||||
|
VERSION = $(shell .venv/bin/python -m setuptools_scm)
|
||||||
|
|
||||||
|
wheel: dist/ocivm-$(VERSION)-py3-none-any.whl
|
||||||
|
|
||||||
|
.venv:
|
||||||
|
python3 -m venv .venv
|
||||||
|
|
||||||
|
venv: .venv
|
||||||
|
|
||||||
|
dev: .venv
|
||||||
|
.venv/bin/python -m pip install -r dev-requirements.txt
|
||||||
|
|
||||||
|
dist/ocivm-$(VERSION)-py3-none-any.whl: \
|
||||||
|
$(shell find src -type f -name '*.py') \
|
||||||
|
src/ocivm/kernel.img \
|
||||||
|
MANIFEST.in \
|
||||||
|
pyproject.toml
|
||||||
|
ifeq ($(VERSION),)
|
||||||
|
$(error Run make dev first)
|
||||||
|
endif
|
||||||
|
.venv/bin/python -m build
|
||||||
|
|
||||||
|
kernel: src/ocivm/kernel.img
|
||||||
|
|
||||||
|
src/ocivm/kernel.img: kconfig
|
||||||
|
cp -uv kconfig kernel/.config
|
||||||
|
$(MAKE) -C kernel
|
||||||
|
cp -uv kernel/arch/x86/boot/bzImage src/ocivm/kernel.img
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(MAKE) -C kernel mrproper
|
||||||
|
rm -rf .venv
|
||||||
|
rm -rf build dist
|
||||||
|
rm -f src/ocivm/kernel.img
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: \
|
||||||
|
clean \
|
||||||
|
dev \
|
||||||
|
kernel \
|
||||||
|
venv \
|
||||||
|
wheel
|
|
@ -0,0 +1,14 @@
|
||||||
|
# ocivm
|
||||||
|
|
||||||
|
Create and run virtual machines from OCI container images.
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* [*buildah*][0]
|
||||||
|
* [*guestfs-tools*][1] (`virt-make-fs`)
|
||||||
|
* [*qemu*][2] >= 4.2
|
||||||
|
|
||||||
|
[0]: https://buildah.io/
|
||||||
|
[1]: https://libguestfs.org/
|
||||||
|
[2]: https://www.qemu.org/
|
|
@ -0,0 +1,3 @@
|
||||||
|
build
|
||||||
|
setuptools_scm[toml]>=6.2
|
||||||
|
-e .
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1ac8758e027247774464c808447a9c2f1f97b637
|
|
@ -0,0 +1,40 @@
|
||||||
|
[project]
|
||||||
|
name = 'ocivm'
|
||||||
|
authors = [
|
||||||
|
{name = 'Dustin C. Hatch'},
|
||||||
|
]
|
||||||
|
description = 'Run an OCI container image as a QEMU microvm'
|
||||||
|
license = {text = 'Apache-2.0 OR MIT'}
|
||||||
|
dynamic = ['version']
|
||||||
|
requires-python = '>=3.10'
|
||||||
|
dependencies = [
|
||||||
|
'rich',
|
||||||
|
'xdg',
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
ocivm = 'ocivm.cli:main'
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ['setuptools>=45', 'setuptools-scm[toml]>=6.2']
|
||||||
|
build-backend = 'setuptools.build_meta'
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
skip-string-normalization = true
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
line_length = 79
|
||||||
|
ensure_newline_before_comments = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
include_trailing_comma = true
|
||||||
|
lines_after_imports = 2
|
||||||
|
multi_line_output = 3
|
||||||
|
use_parentheses = true
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
venvPath = '.'
|
||||||
|
venv = '.venv'
|
||||||
|
pythonVersion = '3.10'
|
|
@ -0,0 +1,2 @@
|
||||||
|
from ocivm.cli import main
|
||||||
|
main()
|
|
@ -0,0 +1,74 @@
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import rich.console
|
||||||
|
import rich.logging
|
||||||
|
import xdg
|
||||||
|
|
||||||
|
from ocivm import image
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CLI:
|
||||||
|
def __init__(self, console: rich.console.Console) -> None:
|
||||||
|
self.console = console
|
||||||
|
|
||||||
|
def image_dir(self) -> Path:
|
||||||
|
image_dir = xdg.xdg_data_home() / 'ocivm' / 'images'
|
||||||
|
log.debug('Using image storage directory %s', image_dir)
|
||||||
|
return image_dir
|
||||||
|
|
||||||
|
def import_image(self, name: str) -> None:
|
||||||
|
img = self.image_dir() / name
|
||||||
|
img.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
img = img.with_suffix('.qcow2')
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.tar') as tmp_tar:
|
||||||
|
with self.console.status('Exporting OCI image as tarball ...'):
|
||||||
|
tar = Path(tmp_tar.name)
|
||||||
|
image.make_tar(name, tar)
|
||||||
|
self.console.print('Created tarball:', tar)
|
||||||
|
with self.console.status('Converting tarball to QCOW2 image ...'):
|
||||||
|
image.tar2qcow2(tar, img)
|
||||||
|
self.console.print('Created QCOW2 image:', img)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
'--quiet',
|
||||||
|
'-q',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Suppress CLI output',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--log-level',
|
||||||
|
default='INFO',
|
||||||
|
choices=('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'),
|
||||||
|
help='Set CLI log level',
|
||||||
|
)
|
||||||
|
sp = parser.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
|
p_import = sp.add_parser('import', help='Import container image')
|
||||||
|
p_import.add_argument('name', help='Container image name')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
console = rich.console.Console(stderr=True, quiet=args.quiet)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.getLevelName(args.log_level),
|
||||||
|
format='%(message)s',
|
||||||
|
datefmt='[%X]',
|
||||||
|
handlers=[rich.logging.RichHandler(console=console)],
|
||||||
|
)
|
||||||
|
cli = CLI(console)
|
||||||
|
match args.command:
|
||||||
|
case 'import':
|
||||||
|
cli.import_image(args.name)
|
|
@ -0,0 +1,87 @@
|
||||||
|
import importlib.resources
|
||||||
|
import logging
|
||||||
|
import string
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .util import list2cmdline
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CONTAINERFILE_TMPL = string.Template(
|
||||||
|
'''\
|
||||||
|
FROM ${name}
|
||||||
|
ADD linuxrc /
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_tar(name: str, dest: Path) -> None:
|
||||||
|
res = importlib.resources.files(__package__).joinpath('linuxrc')
|
||||||
|
with tempfile.TemporaryDirectory() as t:
|
||||||
|
log.debug('Using temporary directory %s', t)
|
||||||
|
log.debug('Copying linuxrc to temporary directory')
|
||||||
|
with res.open('rb') as f:
|
||||||
|
path = Path(t) / 'linuxrc'
|
||||||
|
with path.open('wb') as o:
|
||||||
|
sz = 0
|
||||||
|
while d := f.read(4096):
|
||||||
|
sz += o.write(d)
|
||||||
|
log.debug('Wrote %d bytes', sz)
|
||||||
|
containerfile = CONTAINERFILE_TMPL.safe_substitute(name=name)
|
||||||
|
path = Path(t) / 'Containerfile'
|
||||||
|
with path.open('w', encoding='utf-8') as f:
|
||||||
|
f.write(containerfile)
|
||||||
|
cmd = [
|
||||||
|
'buildah',
|
||||||
|
'build',
|
||||||
|
'--layers',
|
||||||
|
'--output',
|
||||||
|
f'type=tar,dest={dest}',
|
||||||
|
t,
|
||||||
|
]
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
log.debug('Running command: %s', list2cmdline(cmd))
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tar2qcow2(src: Path, dest: Path, size: str = '+1G') -> None:
|
||||||
|
raw = dest.with_suffix('.img')
|
||||||
|
cmd = [
|
||||||
|
'virt-make-fs',
|
||||||
|
'-F',
|
||||||
|
'raw',
|
||||||
|
'-t',
|
||||||
|
'ext4',
|
||||||
|
'--size',
|
||||||
|
size,
|
||||||
|
src,
|
||||||
|
raw,
|
||||||
|
]
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
cmd = [
|
||||||
|
'qemu-img',
|
||||||
|
'convert',
|
||||||
|
'-O',
|
||||||
|
'qcow2',
|
||||||
|
raw,
|
||||||
|
dest,
|
||||||
|
]
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
raw.unlink()
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# vim: set sw=4 ts=4 sts=4 et :
|
||||||
|
|
||||||
|
mkdir -p \
|
||||||
|
/dev \
|
||||||
|
/proc \
|
||||||
|
/run \
|
||||||
|
/sys \
|
||||||
|
/tmp
|
||||||
|
|
||||||
|
mountpoint -q /dev || mount -t devtmpfs devtmpfs /dev
|
||||||
|
mkdir -p /dev/pts /dev/shm
|
||||||
|
mount -t devpts devpts /dev/pts
|
||||||
|
mount -t tmpfs tmpfs /dev/shm
|
||||||
|
ln -s /proc/self/fd /dev/fd
|
||||||
|
mount -t proc proc /proc
|
||||||
|
mount -t sysfs sysfs /sys
|
||||||
|
mount -t tmpfs tmpfs /tmp
|
||||||
|
mount -t tmpfs tmpfs /run
|
||||||
|
|
||||||
|
mkdir -p /tmp/build
|
||||||
|
mount -t 9p -o trans=virtio,version=9p2000.L,msize=52428800 hostfiles /tmp/build
|
||||||
|
|
||||||
|
ip link set eth0 up
|
||||||
|
ip address add 192.168.76.8/24 dev eth0
|
||||||
|
ip route add default via 192.168.76.2
|
||||||
|
echo nameserver 192.168.76.3 > /etc/resolv.conf
|
||||||
|
|
||||||
|
cd /tmp/build
|
||||||
|
exec /bin/bash
|
||||||
|
|
||||||
|
echo 1 > /proc/sys/kernel/sysrq
|
||||||
|
echo b > /proc/sysrq-trigger
|
|
@ -0,0 +1,6 @@
|
||||||
|
import shlex
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def list2cmdline(args: list[Any]) -> str:
|
||||||
|
return ' '.join(shlex.quote(a) for a in args)
|
Loading…
Reference in New Issue