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