Initial commit

master
Dustin 2023-02-23 17:00:30 -06:00
commit 6a31ac392a
16 changed files with 2726 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.mypy_cache/
/.venv
/build/
/dist/
__pycache__/
*.egg-info/
*.py[co]
/kernel.img
src/ocivm/kernel.img

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "kernel"]
path = kernel
url = https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include src/ocivm/kernel.img
include src/ocivm/linuxrc

42
Makefile Normal file
View File

@ -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

14
README.md Normal file
View File

@ -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/

3
dev-requirements.txt Normal file
View File

@ -0,0 +1,3 @@
build
setuptools_scm[toml]>=6.2
-e .

2410
kconfig Normal file

File diff suppressed because it is too large Load Diff

1
kernel Submodule

@ -0,0 +1 @@
Subproject commit 1ac8758e027247774464c808447a9c2f1f97b637

40
pyproject.toml Normal file
View File

@ -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
src/ocivm/__init__.py Normal file
View File

2
src/ocivm/__main__.py Normal file
View File

@ -0,0 +1,2 @@
from ocivm.cli import main
main()

74
src/ocivm/cli.py Normal file
View File

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

87
src/ocivm/image.py Normal file
View File

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

33
src/ocivm/linuxrc Executable file
View File

@ -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
src/ocivm/py.typed Normal file
View File

6
src/ocivm/util.py Normal file
View File

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