Compare commits

..

No commits in common. "b56f8facf39594b27e192c906648dc8a548fe979" and "c587f511bc2969c19340070a91152c59d83c5f45" have entirely different histories.

20 changed files with 1671 additions and 95 deletions

2
.gitignore vendored
View File

@ -3,5 +3,3 @@
*.qcow2
repo/
_build/
*.repo/
*.tar

View File

@ -39,7 +39,7 @@ endif
export CHECKPOINTS=build
IMAGETYPES := regular ostree
FORMATS := oci.tar repo tar
FORMATS := img qcow2 oci.tar repo rootfs ext4 tar
COMMON_TARGETS := qemu
HOST_TARGETS := $(COMMON_TARGETS) $($(HOST_ARCH)_TARGETS)
ALL_TARGETS := $(COMMON_TARGETS) $(foreach a,$(ARCHES), $($(a)_TARGETS))
@ -66,7 +66,9 @@ help:
@echo
@echo Other extensions are also supported:
@echo \ \* .repo: Generate a repo with an ostree commit \(only works for ostree targets\)
@echo \ \* .rootfs: Generate a directory with the rootfs content
@echo \ \* .tar: Generate a tar file with the rootfs content
@echo \ \* .ext4: Generate an ext4 filesystem with the rootfs content \(size from \"image_size\"\)
@echo \ \* oci.tar: Generate an oci container image with the rootfs content
@echo
@echo You can pass variable declarations to osbuild-mpp with the DEFINES make variable.
@ -74,19 +76,28 @@ help:
@echo For example, to add extra rpms to a minimal regular image, use:
@echo " make cs9-qemu-minimal-regular.$(HOST_ARCH).qcow2 DEFINES='extra_rpms=[\"gdb\",\"strace\"]'"
@echo
@echo To easily run the image with qemu, you can use the included runvm tool, like:
@echo \ \ ./runvm cs9-qemu-minimal-regular.$(HOST_ARCH).qcow2
@echo
@echo There are some additional targets:
@echo \ \ manifests: generates resolved json manifests for all images without building them.
@echo \ \ clean_caches: Removes intermediate image build artifacts \(that improve rebuild speed\)
@echo \ \ clean_downloads: Removes files downloaded during image builds
@echo \ \ clean: Run clean_caches and clean_downloads
@echo \ \ osbuildvm-images: Build a image that can be used to build images inside a VM
@echo
@echo There are also some common conversion rules:
@echo \ \ foo.ext4.simg will build foo.ext4 and then convert it with img2simg
@echo \ \ foo.simg will build foo.img and then convert it with img2simg
@echo \ \ foo.tar.gz will build $foo.tar and then gzip it
@echo
@echo "When building a custom variant of an image (say with an extra package) you can use a"
@echo custom @suffix to change the name of the produced file. For example:
@echo " make cs9-qemu-minimal-ostree@gdb.$(HOST_ARCH).qcow2 DEFINES='extra_rpms=[\"gdb\"]'"
@echo
@echo If you pass VM=1, then the images used from \"make osbuildvm-images\" will be used to do the
@echo actual building. This means that you don\'t need sudo rights to run osbuild, and it means
@echo architectures other than the current ones can be built.
@echo
@echo Available image targets \(for $(HOST_ARCH)\) are:
@echo
@ -170,8 +181,30 @@ clean_caches:
.PHONY: clean
clean: clean_downloads clean_caches
ifeq ($(VM), 1)
VM_SUDO=
VM_OSBUILD="osbuildvm/osbuildvm --arch=$(HOST_ARCH)"
else
VM_SUDO=sudo
VM_OSBUILD=sudo osbuild
endif
.PHONY: osbuildvm-images
osbuildvm-images: $(BUILDDIR)
osbuild-mpp osbuildvm/osbuildvm.mpp.yml _build/osbuildvm-$(HOST_ARCH).json
$(VM_OSBUILD) --store $(STOREDIR) --output-directory $(OUTPUTDIR) --export osbuildvm _build/osbuildvm-$(HOST_ARCH).json
cp $(OUTPUTDIR)/osbuildvm/disk.qcow2 _build/osbuildvm-$(HOST_ARCH).img
cp $(OUTPUTDIR)/osbuildvm/initramfs _build/osbuildvm-$(HOST_ARCH).initramfs
cp $(OUTPUTDIR)/osbuildvm/vmlinuz _build/osbuildvm-$(HOST_ARCH).vmlinuz
$(VM_SUDO) rm -rf $(OUTPUTDIR)/osbuildvm
%.ext4.simg : %.ext4
img2simg $< $@
rm $<
%.simg : %.img
img2simg $< $@
rm $<
%.tar.gz : %.tar
gzip -f $<

View File

@ -1,6 +1,6 @@
This software was forked from: https://gitlab.com/CentOS/automotive/sample-images
Copyright Red Hat
Copyright (c) 2021 AutoBase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,20 +0,0 @@
# This service runs once each boot to remove potential leftover
# container state from previous boots.
# This is needed as we're using transient mode in podman where the
# database and other configs are stored in tmpfs, but some other files
# are not. If we don't run this after ane unclean boot then there may
# be leftover files that collect over time.
[Unit]
Description=Clean up podman transient data
RequiresMountsFor=%t/containers
Requires=boot-complete.target
After=local-fs.target boot-complete.target
[Service]
Type=oneshot
ExecStart=/usr/bin/podman system prune --external
[Install]
WantedBy=multi-user.target

View File

@ -3,11 +3,6 @@
driver = "overlay"
runroot = "/run/containers/storage"
graphroot = "/var/lib/containers/storage"
# Enables a global transient storaga mode where all container metadata is stored on non-persistant media
# This guaranteea a fresh state on boot.
# However it is not compabible with a traditional model where containers persist across reboots.
# Use with `podman-clean-transient.service`
transient_store = true
[storage.options]
# We add a custom "/usr/share/containers/storage" here to allow readonly in-image containers

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
podman build --no-cache \
--build-arg commit=cs9-qemu-container-ostree.x86_64.repo \
-f ./hosting/Dockerfile \
-t j7s-os:latest .

View File

@ -33,15 +33,15 @@ pipelines:
- mpp-eval: distro_repos
- mpp-eval: target_repos
- mpp-eval: extra_repos
- - id: copr-podman
baseurl: https://download.copr.fedorainfracloud.org/results/alexl/podman-snapshot/centos-stream-9-x86_64/
- - id: copr-quadlet
baseurl: https://download.copr.fedorainfracloud.org/results/alexl/quadlet/centos-stream-9-$arch/
packages:
mpp-join:
- mpp-eval: base_rpms
- mpp-eval: image_rpms
- mpp-eval: extra_rpms
- - podman
- podman-quadlet
- quadlet
- curl
excludes:
- dracut-config-rescue
@ -53,20 +53,11 @@ pipelines:
mpp-embed:
id: storage.conf
path: ../files/storage.conf
inlinefile2:
type: org.osbuild.files
origin: org.osbuild.source
mpp-embed:
id: podman-clean-transient.service
path: ../files/podman-clean-transient.service
options:
paths:
- from:
mpp-format-string: input://inlinefile/{embedded['storage.conf']}
to: tree:///etc/containers/storage.conf
- from:
mpp-format-string: input://inlinefile2/{embedded['podman-clean-transient.service']}
to: tree:///etc/systemd/system/podman-clean-transient.service
- type: org.osbuild.copy
inputs:
inlinefile:
@ -120,6 +111,5 @@ pipelines:
enabled_services:
- NetworkManager.service
- rngd.service
- podman-clean-transient
- mpp-import-pipelines:
path: include/image.ipp.yml

View File

@ -47,6 +47,12 @@ pipelines:
- type: org.osbuild.locale
options:
language: en_US.UTF-8
- type: org.osbuild.users
options:
users:
guest:
password:
mpp-eval: guest_password
- type: org.osbuild.systemd
options:
enabled_services:

View File

@ -0,0 +1,50 @@
version: '2'
mpp-vars:
efiarch: x64
boot_rpms:
mpp-join:
- mpp-eval: boot_rpms
- - grub2-efi-x64
- grub2-pc
base_rpms:
mpp-join:
- mpp-eval: base_rpms
- - microcode_ctl
pipelines:
- name: build
runner: org.osbuild.centos9
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $distro_module_id
baseurl: $distro_baseurl/BaseOS/$arch/os/
repos:
mpp-eval: distro_repos
packages:
mpp-join:
- mpp-eval: build_rpms
- mpp-eval: extra_build_rpms
- - grub2-efi-x64
- grub2-efi-x64-cdboot
- grub2-tools-efi
- grub2-pc
- grub2-pc-modules
- grub2-tools
- shim-x64
options:
gpgkeys:
- mpp-eval: centos_gpg_key
- mpp-eval: redhat_gpg_key
exclude:
docs: true
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts
labels:
/usr/bin/cp: system_u:object_r:install_exec_t:s0
/usr/bin/tar: system_u:object_r:install_exec_t:s0

View File

@ -3,46 +3,36 @@ version: '2'
mpp-vars:
distro_name: cs9 # The default
mpp-define-image:
size: $image_size
table:
uuid: $parttab_uuid
label: $partition_label
partitions:
- id: efi
start:
mpp-eval: "0 if partition_label == 'gpt' else 2048"
size: $efipart_size
type:
mpp-eval: "'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' if partition_label == 'gpt' else 'ef'"
uuid: $efipart_uuid
- id: boot
size: $bootpart_size
type:
mpp-eval: "'0FC63DAF-8483-4772-8E79-3D69D8477DE4' if partition_label == 'gpt' else '83'"
uuid: $bootpart_uuid
- id: root
type:
mpp-eval: "'0FC63DAF-8483-4772-8E79-3D69D8477DE4' if partition_label == 'gpt' else '83'"
uuid: $rootpart_uuid
pipelines:
- mpp-import-pipelines:
path: distro/$distro_name.ipp.yml
- mpp-import-pipelines:
path: defaults.ipp.yml
- name: build
runner: org.osbuild.centos9
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $distro_module_id
baseurl: $distro_baseurl/BaseOS/$arch/os/
repos:
mpp-eval: distro_repos
packages:
mpp-join:
- mpp-eval: build_rpms
- mpp-eval: extra_build_rpms
- - grub2-efi-x64
- grub2-efi-x64-cdboot
- grub2-tools-efi
- grub2-pc
- grub2-pc-modules
- grub2-tools
- shim-x64
options:
gpgkeys:
- mpp-eval: centos_gpg_key
- mpp-eval: redhat_gpg_key
exclude:
docs: true
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts
labels:
/usr/bin/cp: system_u:object_r:install_exec_t:s0
/usr/bin/tar: system_u:object_r:install_exec_t:s0
- mpp-import-pipelines:
path: target-$target.ipp.yml
- mpp-import-pipeline:
path: build-$arch.ipp.yml
id: build
runner: org.osbuild.centos9

View File

@ -5,17 +5,36 @@ mpp-vars:
default_ostree_ref: $distro_name/$arch/$target-$name
default_ostree_os_version: $distro_version
default_osname: centos
default_uefi_vendor: centos
default_kernel_rpm: kernel
default_linux_firmware_rpm: linux-firmware
default_partition_label: gpt
default_extra_rpms: []
default_extra_build_rpms: []
default_extra_repos: []
default_target_repos: []
default_root_password: $6$xoLqEUz0cGGJRx01$H3H/bFm0myJPULNMtbSsOFd/2BnHqHkMD92Sfxd.EKM9hXTWSmELG8cf205l6dktomuTcgKGGtGDgtvHVXSWU.
default_guest_password: $6$xoLqEUz0cGGJRx01$H3H/bFm0myJPULNMtbSsOFd/2BnHqHkMD92Sfxd.EKM9hXTWSmELG8cf205l6dktomuTcgKGGtGDgtvHVXSWU.
default_root_ssh_key: ""
default_ssh_permit_root_login: false
default_image_size: '8589934592'
default_efipart_size: 204800
default_bootpart_size: 614400
default_kernel_loglevel: 4
static_uuids:
mpp-eval: locals().get('static_uuids', True)
default_rootfs_uuid:
mpp-eval: ('76a22bf4-f153-4541-b6c7-0332c0dfaeac' if static_uuids else str(__import__('uuid').uuid4()))
default_kernel_loglevel: 4
default_bootfs_uuid:
mpp-eval: ('156f0420-627b-4151-ae6f-fda298097515' if static_uuids else str(__import__('uuid').uuid4()))
default_parttab_uuid:
mpp-eval: ('d209c89e-ea5e-4fbd-b161-b461cce297e0' if static_uuids else str(__import__('uuid').uuid4()))
default_efipart_uuid:
mpp-eval: ('68b2905b-df3e-4fb3-80fa-49d1e773aa33' if static_uuids else str(__import__('uuid').uuid4()))
default_bootpart_uuid:
mpp-eval: ('61b2905b-df3e-4fb3-80fa-49d1e773aa32' if static_uuids else str(__import__('uuid').uuid4()))
default_rootpart_uuid:
mpp-eval: ('6264d520-3fb9-423f-8ab8-7a0a8e3d3562' if static_uuids else str(__import__('uuid').uuid4()))
default_use_efi_runtime: true
default_kernel_opts:
- console=tty0
@ -67,6 +86,10 @@ mpp-vars:
mpp-eval: locals().get('ostree_os_version', default_ostree_os_version)
osname:
mpp-eval: locals().get('osname', default_osname)
uefi_vendor:
mpp-eval: locals().get('uefi_vendor', default_uefi_vendor)
partition_label:
mpp-eval: locals().get('partition_label', default_partition_label)
extra_rpms:
mpp-eval: locals().get('extra_rpms', default_extra_rpms)
extra_build_rpms:
@ -75,10 +98,26 @@ mpp-vars:
mpp-eval: locals().get('extra_repos', default_extra_repos)
target_repos:
mpp-eval: locals().get('target_repos', default_target_repos)
image_size:
mpp-eval: locals().get('image_size', default_image_size)
rootfs_uuid:
mpp-eval: locals().get('rootfs_uuid', default_rootfs_uuid)
bootfs_uuid:
mpp-eval: locals().get('bootfs_uuid', default_bootfs_uuid)
parttab_uuid:
mpp-eval: locals().get('parttab_uuid', default_parttab_uuid)
rootpart_uuid:
mpp-eval: locals().get('rootpart_uuid', default_rootpart_uuid)
bootpart_uuid:
mpp-eval: locals().get('bootpart_uuid', default_bootpart_uuid)
efipart_uuid:
mpp-eval: locals().get('efipart_uuid', default_efipart_uuid)
kernel_opts:
mpp-eval: locals().get('kernel_opts', default_kernel_opts)
efipart_size:
mpp-eval: locals().get('efipart_size', default_efipart_size)
bootpart_size:
mpp-eval: locals().get('bootpart_size', default_bootpart_size)
dracut_add_modules:
mpp-eval: locals().get('dracut_add_modules', default_dracut_add_modules)
dracut_omit_modules:
@ -89,6 +128,14 @@ mpp-vars:
mpp-eval: locals().get('dracut_add_drivers', default_dracut_add_drivers)
dracut_install:
mpp-eval: locals().get('dracut_install', default_dracut_install)
root_password:
mpp-eval: locals().get('root_password', default_root_password)
root_ssh_key:
mpp-eval: locals().get('root_ssh_key', default_root_ssh_key)
ssh_permit_root_login:
mpp-eval: locals().get('ssh_permit_root_login', default_ssh_permit_root_login)
guest_password:
mpp-eval: locals().get('guest_password', default_guest_password)
ostree_repo_url:
mpp-eval: locals().get('ostree_repo_url', default_ostree_repo_url)
ostree_remote_name:
@ -190,6 +237,24 @@ mpp-vars:
iA==
=+Gxh
-----END PGP PUBLIC KEY BLOCK-----
fstab:
- uuid:
mpp-eval: rootfs_uuid
vfs_type: ext4
path: /
freq: 1
passno: 1
- label: ESP
vfs_type: vfat
path: /boot/efi
freq: 1
passno: 1
- uuid:
mpp-eval: bootfs_uuid
vfs_type: ext4
path: /boot
freq: 1
passno: 1
build_rpms:
- dnf
- dosfstools
@ -210,12 +275,9 @@ mpp-vars:
boot_rpms:
- dracut-config-generic
- grub2-tools-minimal
- grub2-efi-x64
- grub2-pc
- $kernel_rpm
base_rpms:
- $linux_firmware_rpm
- microcode_ctl
- NetworkManager
- audit
- chrony

View File

@ -96,3 +96,114 @@ pipelines:
mpp-if: ostree_ref in locals().get("ostree_parent_refs", {})
then:
mpp-eval: ostree_parent_refs[ostree_ref]
- name: image-tree
build: name:build
stages:
- type: org.osbuild.ostree.init-fs
- type: org.osbuild.ostree.pull
options:
repo: /ostree/repo
remote:
mpp-eval: ostree_remote_name
inputs:
commits:
type: org.osbuild.ostree
origin: org.osbuild.pipeline
references:
name:ostree-commit:
ref:
mpp-eval: ostree_ref
- type: org.osbuild.ostree.os-init
options:
osname:
mpp-eval: osname
- type: org.osbuild.ostree.config
options:
repo: /ostree/repo
config:
sysroot:
readonly: true
bootloader: none
- type: org.osbuild.ostree.remotes
options:
repo: /ostree/repo
remotes:
- name:
mpp-eval: ostree_remote_name
url:
mpp-eval: ostree_repo_url
- type: org.osbuild.mkdir
options:
paths:
- path: /boot/efi
mode: 448
- type: org.osbuild.ostree.deploy
options:
osname:
mpp-eval: osname
ref:
mpp-eval: ostree_ref
remote:
mpp-eval: ostree_remote_name
mounts:
- /boot
- /boot/efi
rootfs:
label: root
kernel_opts:
mpp-eval: kernel_opts
- type: org.osbuild.ostree.fillvar
options:
deployment:
osname:
mpp-eval: osname
ref:
mpp-eval: ostree_ref
- type: org.osbuild.users
mounts:
- type: org.osbuild.ostree.deployment
name: ostree.deployment
options:
deployment:
osname:
mpp-eval: osname
ref:
mpp-eval: ostree_ref
options:
users:
root:
password:
mpp-eval: root_password
key:
mpp-eval: root_ssh_key
- type: org.osbuild.fstab
options:
ostree:
deployment:
osname:
mpp-eval: osname
ref:
mpp-eval: ostree_ref
filesystems:
mpp-eval: fstab
- type: org.osbuild.ostree.selinux
options:
deployment:
osname:
mpp-eval: osname
ref:
mpp-eval: ostree_ref
- type: org.osbuild.grub2
options:
rootfs:
label: root
bootfs:
label: boot
uefi:
vendor:
mpp-eval: uefi_vendor
unified: false
install: true
legacy: true
write_defaults: false
greenboot: true

View File

@ -0,0 +1,73 @@
version: '2'
mpp-vars:
image_rpms:
mpp-join:
- mpp-eval: locals().get('extra_image_rpms', [])
- mpp-eval: boot_rpms
- mpp-eval: locals().get('extra_boot_rpms', [])
- - shim
pipelines:
- name: image-tree
build: name:build
stages:
mpp-join:
- - type: org.osbuild.copy
inputs:
tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:rootfs
options:
paths:
mpp-join:
- - from: input://tree/
to: tree:///
- type: org.osbuild.users
options:
users:
root:
password:
mpp-eval: root_password
key:
mpp-eval: root_ssh_key
- mpp-eval: target_stages
- - type: org.osbuild.dracut
options:
kernel:
- mpp-eval: rpms['rootfs'][kernel_rpm + '-core'].evra
add_modules:
mpp-eval: dracut_add_modules
omit_modules:
mpp-eval: dracut_omit_modules
add_drivers:
mpp-eval: dracut_add_drivers
filesystems:
mpp-eval: dracut_filesystems
install:
mpp-eval: dracut_install
- type: org.osbuild.fstab
options:
filesystems:
mpp-eval: fstab
- type: org.osbuild.grub2
options:
root_fs_uuid:
mpp-eval: rootfs_uuid
boot_fs_uuid:
mpp-eval: bootfs_uuid
kernel_opts:
mpp-eval: ''' '' .join(kernel_opts)'
uefi:
vendor:
mpp-eval: uefi_vendor
unified: false
legacy: true
write_defaults: false
greenboot: true
- type: org.osbuild.fix-bls
options:
prefix: /
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts

View File

@ -11,10 +11,142 @@ mpp-vars:
then: efi=runtime
- mpp-eval: kernel_opts
pipelines:
# ostree pipeline is in other file.
# Some variables need to be written to files, do that here
- mpp-import-pipelines:
path: image-ostree.ipp.yml
path: image-$image_type.ipp.yml
- name: image
build: name:build
stages:
- type: org.osbuild.truncate
options:
filename: disk.img
size:
mpp-eval: image.size
- type: org.osbuild.sfdisk
devices:
device:
type: org.osbuild.loopback
options:
filename: disk.img
options:
mpp-format-json: '{image.layout}'
- type: org.osbuild.mkfs.fat
devices:
device:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['efi'].start
size:
mpp-eval: image.layout['efi'].size
options:
label: ESP
volid: 7B7795E7
- type: org.osbuild.mkfs.ext4
devices:
device:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['boot'].start
size:
mpp-eval: image.layout['boot'].size
options:
uuid:
mpp-eval: bootfs_uuid
label: boot
- type: org.osbuild.mkfs.ext4
devices:
device:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['root'].start
size:
mpp-eval: image.layout['root'].size
options:
uuid:
mpp-eval: rootfs_uuid
label: root
- type: org.osbuild.copy
inputs:
tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:image-tree
build-tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:build
options:
paths:
mpp-join:
- - from: input://tree/
to: mount://root/
- mpp-eval: locals().get('extra_image_copy_' + image_type, [])
devices:
efi:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['efi'].start
size:
mpp-eval: image.layout['efi'].size
boot:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['boot'].start
size:
mpp-eval: image.layout['boot'].size
root:
type: org.osbuild.loopback
options:
filename: disk.img
start:
mpp-eval: image.layout['root'].start
size:
mpp-eval: image.layout['root'].size
mounts:
- name: root
type: org.osbuild.ext4
source: root
target: /
- name: boot
type: org.osbuild.ext4
source: boot
target: /boot
- name: efi
type: org.osbuild.fat
source: efi
target: /boot/efi
options:
vg_name: osbuild
creation_host: osbuild
description: "Built with osbuild"
- name: qcow2
build: name:build
stages:
- type: org.osbuild.qemu
inputs:
image:
type: org.osbuild.files
origin: org.osbuild.pipeline
references:
name:image:
file: disk.img
options:
filename: disk.qcow2
format:
type: qcow2
compat: '1.1'
- name: container
build: name:build
@ -33,6 +165,85 @@ pipelines:
Cmd:
- "/usr/bin/bash"
# We need a smaller fstab for the non-partitioned case
- name: ext4-fstab
build: name:build
stages:
# We copy /etc to get the right selinux context on the new file
- type: org.osbuild.copy
inputs:
image-tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:image-tree
options:
paths:
- from: input://image-tree/etc
to: tree:///etc
- type: org.osbuild.fstab
options:
filesystems:
- uuid:
mpp-eval: rootfs_uuid
vfs_type: ext4
path: /
- name: ext4
build: name:build
stages:
- type: org.osbuild.truncate
options:
filename: rootfs.ext4
size:
mpp-eval: image.size
- type: org.osbuild.mkfs.ext4
devices:
device:
type: org.osbuild.loopback
options:
filename: rootfs.ext4
start: 0
size:
mpp-format-int: "{int(image.size) // 512}"
options:
uuid:
mpp-eval: rootfs_uuid
label: root
- type: org.osbuild.copy
inputs:
tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:image-tree
fstab:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:ext4-fstab
options:
paths:
mpp-join:
- - from: input://tree/
to: mount://root/
- from: input://fstab/etc/fstab
to: mount://root/etc/fstab
- mpp-eval: locals().get('extra_image_copy_' + image_type, [])
devices:
root:
type: org.osbuild.loopback
options:
filename: rootfs.ext4
start: 0
size:
mpp-format-int: "{int(image.size) // 512}"
mounts:
- name: root
type: org.osbuild.ext4
source: root
target: /
- name: tar
build: name:build
stages:

View File

@ -0,0 +1,2 @@
version: '2'
pipelines: []

View File

@ -1,8 +1,6 @@
echo "========> Building minimal."
sudo podman run --rm \
--privileged \
-v $PWD:/project:Z \
-w /project \
localhost/j7s-os-builder:latest \
make cs9-qemu-container-ostree.x86_64.repo
make cs9-qemu-minimal-ostree.x86_64.repo

394
osbuildvm/osbuildvm Executable file
View File

@ -0,0 +1,394 @@
#!/usr/bin/python3
import argparse
import os
import platform
import select
import shlex
import socket
import subprocess
import sys
import tempfile
import threading
import time
import signal
import shutil
import json
BLOCK_SIZE = 64*1024
def read_manifest(path):
if path == "-":
manifest = sys.stdin.read()
else:
with open(path) as f:
manifest = f.read()
return manifest
def parse_arguments(sys_argv):
parser = argparse.ArgumentParser(description="Build operating system images")
parser.add_argument("manifest_path", metavar="MANIFEST",
help="json file containing the manifest that should be built, or a '-' to read from stdin")
parser.add_argument("--store", metavar="DIRECTORY", type=os.path.abspath,
default=".osbuild",
help="directory where intermediary os trees are stored")
parser.add_argument("--checkpoint", metavar="ID", action="append", type=str, default=None,
help="stage to commit to the object store during build (can be passed multiple times)")
parser.add_argument("--export", metavar="ID", action="append", type=str, default=None,
help="object to export, can be passed multiple times")
parser.add_argument("--output-directory", metavar="DIRECTORY", type=os.path.abspath,
help="directory where result objects are stored")
parser.add_argument("--arch", metavar="ARCH", type=str, default=platform.machine(),
help="Arch to build for")
return parser.parse_args(sys_argv[1:])
def local_osbuild(manifest, opts):
cmd = ['osbuild'] + opts + ['-']
try:
p = subprocess.run(cmd, check=True, input=manifest.encode("utf8"), capture_output=True )
except subprocess.CalledProcessError as e:
print(e.output)
sys.exit(e.returncode)
lines = p.stdout.decode("utf8").splitlines()
checkpoints = {}
for l in lines:
p = l.split()
checkpoint = p[0][:-1]
checkpoints[checkpoint] = p[1]
return checkpoints
def extract_dependencies(manifest):
j = json.loads(manifest)
version = j.get("version", None);
if version != '2':
print(f"Unsupported manifest version {version}, only version 2 supported")
sys.exit(1)
sources = j.get("sources", {});
curl = sources.get("org.osbuild.curl", {});
curl_items = curl.get("items", {});
shas = curl_items.keys()
return list(shas)
def find_images(arch):
base_image=f"osbuildvm-{arch}.img"
base_kernel=f"osbuildvm-{arch}.vmlinuz"
base_initrd=f"osbuildvm-{arch}.initramfs"
image_dir=None
image_dirs = [os.getcwd(), os.path.join(os.getcwd(), "_build"), "/usr/share/osbuildvm"]
for i in image_dirs:
if os.path.exists(os.path.join(i, base_image)):
image_dir = i
break
if not image_dir:
print(f"Unable to find {base_image}, tried: {image_dirs}", file=sys.stderr)
sys.exit(1)
image = os.path.join(image_dir, base_image)
kernel = os.path.join(image_dir, base_kernel)
initrd = os.path.join(image_dir, base_initrd)
return (image, kernel, initrd)
def qemu_img(*args):
res = subprocess.run(["qemu-img"] + [*args],
stdout=subprocess.PIPE,
check=True)
class QEmu(object):
def __init__(self, image1, image2, kernel, initrd, arch):
# This is where we store the sockets and the pidfile
self.tmpdir = tempfile.TemporaryDirectory(prefix="tmp-qemu-")
self.args = []
self.pid = 0
self.host_arch = platform.machine()
self.arch = arch
debug_serial = False
qemu_kvm_path = self.find_qemu()
self.args.append(qemu_kvm_path)
# Virtio serial ports
self.args.extend(["-device", "virtio-serial"])
out_socket_path = self.add_socket("output")
sync_socket_path = self.add_socket("sync")
stdout_socket_path = self.add_socket("stdout")
debug_cmdline="quiet loglevel=1"
if debug_serial:
self.args.extend(["-serial", "file:/dev/stdout"])
debug_cmdline=""
# Machine details
self.args.extend(["-m", "size=2G",
"-nodefaults",
"-vga", "none", "-vnc", "none"])
if self.kvm_supported():
self.args.extend(["-enable-kvm"])
if self.arch=="x86_64":
machine = "q35"
cpu = "qemu64"
if self.arch=="aarch64":
machine = "virt"
cpu = "cortex-a57"
if self.arch == self.host_arch:
cpu = "host"
self.args.extend(["-machine", machine,
"-cpu", cpu])
if self.arch=="aarch64":
self.args.extend(["-bios", "/usr/share/edk2/aarch64/QEMU_EFI.fd",
"-boot", "efi"])
pid_file = os.path.join(self.tmpdir.name, "qemu.pid")
self.args.extend(["-daemonize", "-pidfile" , pid_file,
"-kernel", kernel,
"-initrd", initrd,
"-append", f'root=/dev/vda console=ttyS0 init=/usr/bin/start.sh {debug_cmdline} ro',
"-drive", f"file={image1},index=0,media=disk,format=qcow2,snapshot=on,if=virtio",
"-drive", f"file={image2},index=1,media=disk,format=raw,if=virtio"])
p = subprocess.run(self.args, check=True)
with open(pid_file, "r") as f:
self.pid = int(f.read())
self.sock_out = self.connect_socket(out_socket_path)
self.sock_sync = self.connect_socket(sync_socket_path)
self.sock_stdout = self.connect_socket(stdout_socket_path)
def connect_socket(self, path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(path)
return sock
def add_socket(self, id):
socket_path = os.path.join(self.tmpdir.name, id + ".sock")
self.args.extend(["-chardev", f"socket,path={socket_path},server=on,wait=off,id={id}",
"-device", f"virtserialport,chardev={id},name={id}"])
return socket_path
def find_qemu(self):
if self.arch == self.host_arch:
binary_name = "qemu-kvm"
else:
binary_name = f"qemu-system-{self.arch}"
for d in ["/usr/bin", "/usr/libexec"]:
p = os.path.join(d, binary_name)
if os.path.isfile(p):
return p
print(f"Can't find {binary_name}", file=sys.stderr)
sys.exit(1)
def kvm_supported(self):
return self.arch == self.host_arch and os.path.exists("/dev/kvm")
def copy_out(self, destination):
while True:
readable, writable, exceptional = select.select([self.sock_out, self.sock_sync, self.sock_stdout], [], [])
read_something = False
if self.sock_stdout in readable:
data = self.sock_stdout.recv(BLOCK_SIZE)
while len(data) > 0:
res = sys.stdout.buffer.write(data)
data = data[res:]
sys.stdout.flush()
read_something = True
if self.sock_out in readable:
data = self.sock_out.recv(BLOCK_SIZE)
while len(data) > 0:
res = destination.write(data)
data = data[res:]
read_something = True
if read_something:
continue # Don't exit until there is no more to read from sock or sock_stdout
# If we had no buffered data in sock and sync_sock is readable that means we copied everything and can exit
if self.sock_sync in readable:
data = self.sock_sync.recv(BLOCK_SIZE)
break
def kill(self):
if self.pid != 0:
os.kill(self.pid, signal.SIGTERM)
self.pid = 0
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.kill()
self.tmpdir.cleanup()
def create_ext4_image(path, size, root_dir):
with open(path, "w") as f:
f.truncate(size)
cmd = ["mkfs.ext4", "-d", root_dir, "-E", "no_copy_xattrs,root_owner=0:0", "-O", "^has_journal", path]
try:
p = subprocess.run(cmd, check=True,capture_output=True)
except subprocess.CalledProcessError as e:
print(f"Unable to create ext4 work fs: {e}", file=sys.stderr)
sys.exit(e.returncode)
def link_or_copy_file(source_path, dest_path):
try:
os.link(source_path, dest_path)
except Exception as e:
shutil.copyfile(source_path, dest_path, follow_symlinks=False)
def make_work_rootfs(args, tmpdirname, manifest, digests, main_sh, checkpoint_ids):
rootdir = os.path.join(tmpdirname, "root")
os.makedirs(rootdir, exist_ok=True)
with open (os.open(os.path.join(rootdir, "main.sh"), os.O_CREAT | os.O_WRONLY, 0o775), "w") as mainsh:
mainsh.write(main_sh)
with open (os.path.join(rootdir, "image.json"), "w") as image_json:
image_json.write(manifest)
orig_sources_dir = os.path.join(args.store, "sources/org.osbuild.files")
root_sources_dir = os.path.join(rootdir, "osbuild_store/sources/org.osbuild.files")
os.makedirs(root_sources_dir, mode=0o777, exist_ok=True)
root_input_dir = os.path.join(rootdir, "input")
os.makedirs(root_input_dir, mode=0o777, exist_ok=True)
for digest in digests:
link_or_copy_file(os.path.join(orig_sources_dir, digest),
os.path.join(root_sources_dir, digest))
for cp, cp_id in checkpoint_ids.items():
source_path = os.path.join(args.store, "refs_tars/" + cp_id + ".tar.gz")
if os.path.isfile(source_path):
link_or_copy_file(source_path,
os.path.join(root_input_dir, cp_id + ".tar.gz"))
return rootdir
# Moves any files recursively in root_src_dir to root_dst_dir, replacing if needed
# Keeps old files and directories in root_dst_dir
def move_merged(root_src_dir, root_dst_dir):
root_src_dir = os.path.abspath(root_src_dir)
root_dst_dir = os.path.abspath(root_dst_dir)
for src_dir, dirs, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
os.remove(dst_file)
shutil.move(src_file, dst_dir)
def run_in_vm(args, manifest, digests, tmpdirname, shell_to_run, checkpoint_ids):
image,kernel,initrd = find_images(args.arch)
rootdir = make_work_rootfs(args, tmpdirname, manifest, digests, shell_to_run, checkpoint_ids)
work_image = os.path.join(tmpdirname, "work.img")
create_ext4_image(work_image, 100*1024*1024*1024, rootdir)
shutil.rmtree(rootdir)
output_path = args.output_directory
os.makedirs(output_path, exist_ok=True)
exit_status = 1
with tempfile.TemporaryDirectory(prefix="download-", dir=output_path) as output_tmpdir:
with QEmu(image, work_image, kernel, initrd, args.arch) as qemu:
with subprocess.Popen(["tar", "x", "-C", output_tmpdir], stdin=subprocess.PIPE) as proc:
qemu.copy_out(proc.stdin)
exit_status_path = os.path.join(output_tmpdir, "exit_status")
if os.path.isfile(exit_status_path):
with open(exit_status_path, "r") as f:
exit_status = int(f.read())
# Move osbuild outout to the real output dir
osbuild_output = os.path.join(output_tmpdir, "osbuild")
if os.path.isdir(osbuild_output):
move_merged(osbuild_output, output_path)
checkpoints_output = os.path.join(output_tmpdir, "checkpoints")
checkpoints_dest = os.path.join(args.store, "refs_tars")
if os.path.isdir(checkpoints_output):
os.makedirs(checkpoints_dest, exist_ok=True)
for tar in os.listdir(checkpoints_output):
if not os.path.isfile(os.path.join(checkpoints_dest, tar)):
shutil.move(os.path.join(checkpoints_output, tar),
checkpoints_dest)
sys.exit(exit_status)
def main():
args = parse_arguments(sys.argv)
manifest = read_manifest(args.manifest_path)
print("Running osbuild on host to download files")
checkpoint_ids = local_osbuild(manifest, ['--store', args.store])
digests = extract_dependencies(manifest)
mainsh_data = f'''\
#!/bin/bash
mkdir -p /work/osbuild_store
(
echo === Extracting checkpoints in vm ===
for tar in $(find /work/input/ -mindepth 1 -print ); do
echo extracting $(basename $tar)
tar xf $tar --acls --selinux --xattrs -C /work/osbuild_store
done
echo === Running osbuild in vm ===
osbuild --store /work/osbuild_store --output-directory /work/output/osbuild {' '.join(map(lambda e: "--export " + e, args.export))} {' '.join(map(lambda cp: "--checkpoint " + cp, args.checkpoint))} /work/image.json
RES=$?
echo $RES > /work/output/exit_status
echo === Osbuild exit status $RES ===
echo === Saving checkpoints ===
mkdir -p /work/output/checkpoints
for cp in $(find /work/osbuild_store/refs/ -mindepth 1 -printf "%f "); do
if test -f /work/input/$cp.tar.gz; then
continue
fi
obj=$(basename $(readlink /work/osbuild_store/refs/$cp))
tar cSf /work/output/checkpoints/$cp.tar.gz --acls --selinux --xattrs -C /work/osbuild_store/ refs/$cp objects/$obj
echo Saved $cp
done
) > /dev/virtio-ports/stdout 2>&1
tar cSf /dev/virtio-ports/output -C /work/output ./
# Signal output ended
sleep 3
echo DONE > /dev/virtio-ports/sync
# Block for it to be fully read
cat /dev/virtio-ports/sync
'''
tmpdir = os.path.join(args.store, "tmp")
os.makedirs(tmpdir, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="osbuild-qemu-", dir=tmpdir) as tmpdirname:
run_in_vm(args, manifest, digests, tmpdirname, mainsh_data, checkpoint_ids)
if __name__ == "__main__":
main()

229
osbuildvm/osbuildvm.mpp.yml Normal file
View File

@ -0,0 +1,229 @@
version: '2'
mpp-vars:
rootfs_uuid: 86a22bf4-f153-4541-b6c7-0332c0dfaead
rootfs_size: 2147483648
cs9_baseurl: http://mirror.stream.centos.org/9-stream
cs9_repos:
- id: baseos
baseurl: $cs9_baseurl/BaseOS/$arch/os/
- id: appstream
baseurl: $cs9_baseurl/AppStream/$arch/os/
centos_gpg_key: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)
mQINBFzMWxkBEADHrskpBgN9OphmhRkc7P/YrsAGSvvl7kfu+e9KAaU6f5MeAVyn
rIoM43syyGkgFyWgjZM8/rur7EMPY2yt+2q/1ZfLVCRn9856JqTIq0XRpDUe4nKQ
8BlA7wDVZoSDxUZkSuTIyExbDf0cpw89Tcf62Mxmi8jh74vRlPy1PgjWL5494b3X
5fxDidH4bqPZyxTBqPrUFuo+EfUVEqiGF94Ppq6ZUvrBGOVo1V1+Ifm9CGEK597c
aevcGc1RFlgxIgN84UpuDjPR9/zSndwJ7XsXYvZ6HXcKGagRKsfYDWGPkA5cOL/e
f+yObOnC43yPUvpggQ4KaNJ6+SMTZOKikM8yciyBwLqwrjo8FlJgkv8Vfag/2UR7
JINbyqHHoLUhQ2m6HXSwK4YjtwidF9EUkaBZWrrskYR3IRZLXlWqeOi/+ezYOW0m
vufrkcvsh+TKlVVnuwmEPjJ8mwUSpsLdfPJo1DHsd8FS03SCKPaXFdD7ePfEjiYk
nHpQaKE01aWVSLUiygn7F7rYemGqV9Vt7tBw5pz0vqSC72a5E3zFzIIuHx6aANry
Gat3aqU3qtBXOrA/dPkX9cWE+UR5wo/A2UdKJZLlGhM2WRJ3ltmGT48V9CeS6N9Y
m4CKdzvg7EWjlTlFrd/8WJ2KoqOE9leDPeXRPncubJfJ6LLIHyG09h9kKQARAQAB
tDpDZW50T1MgKENlbnRPUyBPZmZpY2lhbCBTaWduaW5nIEtleSkgPHNlY3VyaXR5
QGNlbnRvcy5vcmc+iQI3BBMBAgAhBQJczFsZAhsDBgsJCAcDAgYVCAIJCgsDFgIB
Ah4BAheAAAoJEAW1VbOEg8ZdjOsP/2ygSxH9jqffOU9SKyJDlraL2gIutqZ3B8pl
Gy/Qnb9QD1EJVb4ZxOEhcY2W9VJfIpnf3yBuAto7zvKe/G1nxH4Bt6WTJQCkUjcs
N3qPWsx1VslsAEz7bXGiHym6Ay4xF28bQ9XYIokIQXd0T2rD3/lNGxNtORZ2bKjD
vOzYzvh2idUIY1DgGWJ11gtHFIA9CvHcW+SMPEhkcKZJAO51ayFBqTSSpiorVwTq
a0cB+cgmCQOI4/MY+kIvzoexfG7xhkUqe0wxmph9RQQxlTbNQDCdaxSgwbF2T+gw
byaDvkS4xtR6Soj7BKjKAmcnf5fn4C5Or0KLUqMzBtDMbfQQihn62iZJN6ZZ/4dg
q4HTqyVpyuzMXsFpJ9L/FqH2DJ4exGGpBv00ba/Zauy7GsqOc5PnNBsYaHCply0X
407DRx51t9YwYI/ttValuehq9+gRJpOTTKp6AjZn/a5Yt3h6jDgpNfM/EyLFIY9z
V6CXqQQ/8JRvaik/JsGCf+eeLZOw4koIjZGEAg04iuyNTjhx0e/QHEVcYAqNLhXG
rCTTbCn3NSUO9qxEXC+K/1m1kaXoCGA0UWlVGZ1JSifbbMx0yxq/brpEZPUYm+32
o8XfbocBWljFUJ+6aljTvZ3LQLKTSPW7TFO+GXycAOmCGhlXh2tlc6iTc41PACqy
yy+mHmSv
=kkH7
-----END PGP PUBLIC KEY BLOCK-----
pipelines:
- runner: org.osbuild.centos9
name: build
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: platform:el9
baseurl: $cs9_baseurl/BaseOS/$arch/os/
repos:
mpp-eval: cs9_repos
packages:
- dnf
- e2fsprogs
- policycoreutils
- python3-iniparse
- python39
- qemu-img
- selinux-policy-targeted
- tar
- xz
options:
gpgkeys:
- mpp-eval: centos_gpg_key
exclude:
docs: true
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts
labels:
/usr/bin/cp: system_u:object_r:install_exec_t:s0
/usr/bin/tar: system_u:object_r:install_exec_t:s0
- name: rootfs
build: name:build
stages:
- type: org.osbuild.rpm
options:
gpgkeys:
- mpp-eval: centos_gpg_key
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: platform:el9
baseurl: $cs9_baseurl/BaseOS/$arch/os/
repos:
mpp-join:
- mpp-eval: cs9_repos
- - id: osbuild
baseurl: https://download.copr.fedorainfracloud.org/results/@osbuild/osbuild/centos-stream-9-$arch
packages:
- bash
- dracut-config-generic
- kernel
- langpacks-en
- selinux-policy-targeted
- net-tools
- osbuild
- osbuild-tools
- osbuild-ostree
excludes:
- dracut-config-rescue
- type: org.osbuild.locale
options:
language: en_US.UTF-8
- type: org.osbuild.copy
inputs:
inlinefile:
type: org.osbuild.files
origin: org.osbuild.source
mpp-embed:
id: osbuilder.sh
text: |
#!/usr/bin/bash
function clean_up {
systemctl poweroff -f -f
}
trap clean_up EXIT
if grep -q "osbuilder_bash=1" /proc/cmdline; then bash; exit; fi
mount /dev/vdb /work
/work/main.sh
options:
paths:
- from:
mpp-format-string: input://inlinefile/{embedded['osbuilder.sh']}
to: tree:///usr/bin/start.sh
- type: org.osbuild.chmod
options:
items:
/usr/bin/start.sh:
mode: a+x
- type: org.osbuild.mkdir
options:
paths:
- path: /work
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts
- type: org.osbuild.selinux
options:
file_contexts: etc/selinux/targeted/contexts/files/file_contexts
- name: image
build: name:build
stages:
- type: org.osbuild.truncate
options:
filename: disk.img
size:
mpp-format-string: '{rootfs_size}'
- type: org.osbuild.mkfs.ext4
devices:
device:
type: org.osbuild.loopback
options:
filename: disk.img
start: 0
size:
mpp-eval: rootfs_size
options:
uuid:
mpp-eval: rootfs_uuid
label: root
- type: org.osbuild.copy
inputs:
tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:rootfs
options:
paths:
- from: input://tree/
to: mount://root/
devices:
root:
type: org.osbuild.loopback
options:
filename: disk.img
start: 0
size:
mpp-eval: rootfs_size
mounts:
- name: root
type: org.osbuild.ext4
source: root
target: /
- name: osbuildvm
build: name:build
stages:
- type: org.osbuild.qemu
inputs:
image:
type: org.osbuild.files
origin: org.osbuild.pipeline
references:
name:image:
file: disk.img
options:
filename: disk.qcow2
format:
type: qcow2
compat: '1.1'
- type: org.osbuild.copy
inputs:
rootfs:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:rootfs
options:
paths:
- from:
mpp-format-string: input://rootfs/usr/lib/modules/{rpms['rootfs']['kernel-core'].evra}/vmlinuz
to: tree:///vmlinuz
- from:
mpp-format-string: input://rootfs/boot/initramfs-{rpms['rootfs']['kernel-core'].evra}.img
to: tree:///initramfs
- type: org.osbuild.chmod
options:
items:
/initramfs:
mode: a+r

449
runvm Executable file
View File

@ -0,0 +1,449 @@
#!/usr/bin/env python3
import argparse
import atexit
import binascii
import http.server
import os
import platform
import select
import shutil
import signal
import socket
import socketserver
import subprocess
import sys
import tempfile
import time
is_verbose = False
def print_verbose(s):
if is_verbose:
print(s)
def print_error(s):
print(s, file=sys.stderr)
def exit_error(s):
print_error(s)
sys.exit(1)
def bool_arg(val):
return "on" if val else "off"
def find_qemu(arch):
binary_names = [ f"qemu-system-{arch}" ]
if arch == platform.machine():
binary_names.append("qemu-kvm")
for binary_name in binary_names:
if "QEMU_BUILD_DIR" in os.environ:
p = os.path.join(os.environ["QEMU_BUILD_DIR"], binary_name)
if os.path.isfile(p):
return p
else:
exit_error(f"Can't find {binary_name}")
qemu_bin_dirs = ["/usr/bin", "/usr/libexec"]
if "PATH" in os.environ:
qemu_bin_dirs += os.environ["PATH"].split(":")
for d in qemu_bin_dirs:
p = os.path.join(d, binary_name)
if os.path.isfile(p):
return p
exit_error(f"Can't find {binary_name}")
def qemu_available_accels(qemu):
cmd = qemu + ' -accel help'
info = subprocess.check_output(cmd.split(" ")).decode('utf-8')
accel_list = []
for accel in ('kvm', 'xen', 'hvf', 'hax', 'tcg'):
if info.find(accel) > 0:
accel_list.append(accel)
return accel_list
def random_id():
return binascii.b2a_hex(os.urandom(8)).decode('utf8')
def machine_id():
try:
with open("/etc/machine-id", "r") as f:
mid = f.read().strip()
except FileNotFoundError:
if sys.platform == "darwin":
# for macOS
import plistlib
cmd = "ioreg -rd1 -c IOPlatformExpertDevice -a"
plist_data = subprocess.check_output(cmd.split(" "))
mid = plistlib.loads(plist_data)[0]["IOPlatformUUID"].replace("-","")
else:
# fallback for the other distros
hostname = socket.gethostname()
mid = ''.join(hex(ord(x))[2:] for x in (hostname*16)[:16])
return mid
def generate_mac_address():
# create a new mac address based on our machine id
data = machine_id()
maclst = ["FE"] + [data[x:x+2] for x in range(-12, -2, 2)]
return ":".join(maclst)
def run_http_server(path):
writer, reader = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
child_pid = os.fork()
if child_pid == 0:
reader.close()
# Child
os.chdir(path)
class HTTPHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass # Silence logs
httpd = socketserver.TCPServer(("127.0.0.1", 0), HTTPHandler)
writer.send(str(httpd.server_address[1]).encode("utf8"))
writer.close()
httpd.serve_forever()
sys.exit(0)
# Parent
writer.close()
atexit.register(os.kill, child_pid,signal.SIGTERM)
http_port = int(reader.recv(128).decode("utf8"))
reader.close()
return http_port
def find_ovmf(args):
dirs = [
"~/.local/share/ovmf",
"/usr/share/OVMF",
"/usr/share/edk2/ovmf/"
]
if args.ovmf_dir:
dirs.append(args.ovmf_dir)
for d in dirs:
path = os.path.expanduser(d)
if os.path.exists(path):
return path
raise RuntimeError("Could not find OMVF")
# location can differ depending on how qemu is installed
def find_edk2():
dirs = [
"/usr/local/share/qemu",
"/opt/homebrew/share/qemu"
]
for d in dirs:
path = os.path.expanduser(d)
if os.path.exists(path):
return path
raise RuntimeError("Could not find edk2 directory")
def qemu_run_command(qmp_socket_path, command):
sock2 = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock2.connect(qmp_socket_path)
r = sock2.recv(1024)
sock2.send('{"execute":"qmp_capabilities"}\n'.encode("utf8"))
r = sock2.recv(1024)
sock2.send(f'{command}\n'.encode("utf8"))
r = sock2.recv(1024)
sock2.close()
def virtio_serial_connect(virtio_socket_path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
while True:
time.sleep(0.1)
try:
sock.connect(virtio_socket_path)
return sock
except FileNotFoundError:
pass
def available_tcp_port(port_range_from = 1024):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
port = port_range_from
port_range_to = port_range_from + 32 # limit for retry
while port < port_range_to:
try:
s.bind(('', port))
except OSError:
port += 1
continue
break
s.close()
return port
class WatchdogCommand:
START = 1
STOP = 2
def __init__(self, op, arg = None):
self.op = op
self.arg = arg
def parse_watchdog_commands(sock):
commands = []
data = sock.recv(16).decode("utf8")
for l in data.splitlines():
if l.startswith("START"):
try:
arg = int(l[5:])
except ValueError:
arg = 30 # Default if not specified
commands.append( WatchdogCommand(WatchdogCommand.START, arg) )
elif l.startswith("STOP"):
commands.append( WatchdogCommand(WatchdogCommand.STOP) )
else:
print_verbose(f"Unsupported watchdog command {l}")
return commands
def run_watchdog(watch_socket_path, qmp_socket_path):
sock = virtio_serial_connect(watch_socket_path)
p = select.poll()
p.register(sock, select.POLLIN)
watchdog_timeout = None
watchdog_delay = 30
while True:
timeout = None
if watchdog_timeout != None:
timeout = max(watchdog_timeout - time.time(), 0) * 1000
poll_res = p.poll(timeout)
if len(poll_res) > 0:
v = poll_res[0][1]
if v & select.POLLHUP:
sys.exit(0)
commands = parse_watchdog_commands(sock)
for cmd in commands:
if cmd.op == WatchdogCommand.START:
print_verbose(f"Starting watchdog for {cmd.arg} sec")
watchdog_timeout = time.time() + cmd.arg
if cmd.op == WatchdogCommand.STOP:
print_verbose(f"Stopped watchdog")
watchdog_timeout = None
if watchdog_timeout != None and time.time() >= watchdog_timeout:
print_verbose(f"Triggering watchdog")
qemu_run_command(qmp_socket_path, '{"execute": "system_reset"}')
# Queue a new timeout in case the next boot fails, until disabled
watchdog_timeout = time.time() + watchdog_delay
def main():
parser = argparse.ArgumentParser(description="Boot virtual machine images")
parser.add_argument("--verbose", default=False, action="store_true")
parser.add_argument("--arch", default=platform.machine(), action="store",
help=f"Arch to run for (default {platform.machine()})")
parser.add_argument("--publish-dir", action="store",
help=f"Publish the specified directory over http in the vm")
parser.add_argument("--memory", default="2G",
help=f"Memory size (default 2G)")
parser.add_argument("--nographics", default=False, action="store_true",
help=f"Run without graphics")
parser.add_argument("--watchdog", default=False, action="store_true",
help=f"Enable watchdog")
parser.add_argument("--tpm2", default=False, action="store_true",
help=f"Enable TPM2")
parser.add_argument("--snapshot", default=False, action="store_true",
help=f"Work on a snapshot of the image")
parser.add_argument("--ovmf-dir", action="store",
help="Specify directory for OVMF files (Open Virtual Machine Firmware)")
parser.add_argument("--secureboot", dest="secureboot", action="store_true", default=False,
help="Enable SecureBoot")
parser.add_argument("--ssh-port", type=int, default=2222,
help="SSH port forwarding to SSH_PORT (default 2222)")
parser.add_argument("--cdrom", action="store",
help="Specify .iso to load")
parser.add_argument("image", type=str, help="The image to boot")
parser.add_argument('extra_args', nargs=argparse.REMAINDER, metavar="...", help="extra qemu arguments")
args = parser.parse_args(sys.argv[1:])
global is_verbose
is_verbose = args.verbose
# arm64 is an alias for aarch64 on macOS
if args.arch == "arm64":
args.arch = "aarch64"
qemu = find_qemu(args.arch)
accel_list = qemu_available_accels(qemu)
qemu_args = [qemu]
if args.arch == "x86_64":
machine = "q35"
default_cpu = "qemu64,+ssse3,+sse4.1,+sse4.2,+popcnt"
ovmf = find_ovmf(args)
if args.secureboot:
qemu_args += [
"-drive", f"file={ovmf}/OVMF_CODE.secboot.fd,if=pflash,format=raw,unit=0,readonly=on",
"-drive", f"file={ovmf}/OVMF_VARS.secboot.fd,if=pflash,format=raw,unit=1,snapshot=on,readonly=off",
]
else:
qemu_args += [
"-drive", f"file={ovmf}/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on",
"-drive", f"file={ovmf}/OVMF_VARS.fd,if=pflash,format=raw,unit=1,snapshot=on,readonly=off",
]
elif args.arch == "aarch64":
machine = "virt"
default_cpu = "cortex-a57"
if sys.platform == "darwin":
edk2 = find_edk2()
qemu_args += [
"-device", "virtio-gpu-pci", # for display
"-display", "default,show-cursor=on", # for display
"-device", "qemu-xhci", # for keyboard
"-device", "usb-kbd", # for keyboard
"-device", "usb-tablet", # for mouse
"-smp", str(os.cpu_count()), # for max cores
"-drive", f"file={edk2}/edk2-aarch64-code.fd,if=pflash,format=raw,unit=0,readonly=on",
"-drive", f"file={edk2}/edk2-arm-vars.fd,if=pflash,format=raw,unit=1,snapshot=on,readonly=off"
]
else:
qemu_args += [
"-bios", "/usr/share/edk2/aarch64/QEMU_EFI.fd",
"-boot", "efi"
]
else:
exit_error(f"unsupported architecture {args.arch}")
accel_enabled = True
if 'kvm' in accel_list and os.path.exists("/dev/kvm"):
qemu_args += ['-enable-kvm']
elif 'hvf' in accel_list:
qemu_args += ['-accel', 'hvf']
else:
accel_enabled = False
print_verbose("Acceleration: off")
qemu_args += [
"-m", str(args.memory),
"-machine", machine,
"-cpu", "host" if accel_enabled else default_cpu
]
guestfwds=""
if args.publish_dir:
if shutil.which("netcat") is None:
print("Command `netcat` not found in path, ignoring publish-dir")
else:
httpd_port = run_http_server(args.publish_dir)
guestfwds = f"guestfwd=tcp:10.0.2.100:80-cmd:netcat 127.0.0.1 {httpd_port},"
print_verbose(f"publishing {args.publish_dir} on http://10.0.2.100/")
portfwd = {
available_tcp_port(args.ssh_port): 22
}
for local, remote in portfwd.items():
print_verbose(f"port: {local} → {remote}")
fwds = [f"hostfwd=tcp::{h}-:{g}" for h, g in portfwd.items()]
macstr = generate_mac_address()
print_verbose(f"MAC: {macstr}")
qemu_args += [
"-device", f"virtio-net-pci,netdev=n0,mac={macstr}",
"-netdev", "user,id=n0,net=10.0.2.0/24," + guestfwds + ",".join(fwds),
]
if args.nographics:
qemu_args += ["-nographic"]
runvm_id = random_id()
tmpdir = tempfile.TemporaryDirectory(prefix=f"runvm-{runvm_id}")
watchdog_pid = 0
if args.watchdog:
qmp_socket_path = os.path.join(tmpdir.name, "qmp-socket")
watch_socket_path = os.path.join(tmpdir.name, "watch-socket")
qemu_args += [
"-qmp", f"unix:{qmp_socket_path},server=on,wait=off",
"-device", "virtio-serial", "-chardev", f"socket,path={watch_socket_path},server=on,wait=off,id=watchdog",
"-device", "virtserialport,chardev=watchdog,name=watchdog.0"
]
watchdog_pid = os.fork()
if watchdog_pid == 0:
run_watchdog(watch_socket_path, qmp_socket_path)
sys.exit(0)
if args.tpm2:
if shutil.which("swtpm") is None:
exit_error("Command `swtpm` not found in path, this is needed for tpm2 support")
tpm2_socket = os.path.join(tmpdir.name, "tpm-socket")
if args.snapshot:
tpm2_path = os.path.join(tmpdir.name, "tpm2_state")
else:
tpm2_path = ".tpm2_state"
os.makedirs(tpm2_path, exist_ok=True)
swtpm_args = ["swtpm", "socket", "--tpm2", "--tpmstate", f"dir={tpm2_path}", "--ctrl", f"type=unixio,path={tpm2_socket}" ]
res = subprocess.Popen(swtpm_args)
qemu_args += [
"-chardev", f"socket,id=chrtpm,path={tpm2_socket}",
"-tpmdev", "emulator,id=tpm0,chardev=chrtpm",
"-device", "tpm-tis,tpmdev=tpm0"
]
print_verbose(f"Image: {args.image}")
if args.image.endswith(".raw"):
qemu_args += [
"-drive", f"file={args.image},index=0,media=disk,format=raw,if=virtio,snapshot={bool_arg(args.snapshot)}",
]
else: # assume qcow2
qemu_args += [
"-drive", f"file={args.image},index=0,media=disk,format=qcow2,if=virtio,snapshot={bool_arg(args.snapshot)}",
]
if args.cdrom:
qemu_args += [
"-cdrom", args.cdrom,
"-boot", "d"
]
qemu_args += args.extra_args
print_verbose(f"Running: {' '.join(qemu_args)}")
try:
res = subprocess.run(qemu_args, check=False)
except KeyboardInterrupt:
exit_error("Aborted")
if watchdog_pid:
os.kill(watchdog_pid, signal.SIGTERM)
tmpdir.cleanup()
return res.returncode
if __name__ == "__main__":
sys.exit(main())

View File

@ -9,6 +9,7 @@
#
# * Knows what target to export based on the extensions.
# * Knows what the exported filename based on the extensions.
# * Supports building natively or in a VM via osbuildvm.
# * Runs only the minimal required commands as root, and
# chowns the resulting files to the user
# * Supports exporting an extra ostree commit and pulling that
@ -25,14 +26,22 @@ EXTENSION="$6"
# Map extension => export pipeline name
declare -A EXPORT_BY_EXT
EXPORT_BY_EXT[img]=image
EXPORT_BY_EXT[oci.tar]=container
EXPORT_BY_EXT[qcow2]=qcow2
EXPORT_BY_EXT[repo]=ostree-commit
EXPORT_BY_EXT[rootfs]=rootfs
EXPORT_BY_EXT[ext4]=ext4
EXPORT_BY_EXT[tar]=tar
# Map extension to name of exported file by pipeline
declare -A EXPORT_FILE_BY_EXT
EXPORT_FILE_BY_EXT[img]=disk.img
EXPORT_FILE_BY_EXT[qcow2]=disk.qcow2
EXPORT_FILE_BY_EXT[oci.tar]=container.tar
EXPORT_FILE_BY_EXT[repo]=repo
EXPORT_FILE_BY_EXT[rootfs]=
EXPORT_FILE_BY_EXT[ext4]=rootfs.ext4
EXPORT_FILE_BY_EXT[tar]=rootfs.tar
EXPORT=${EXPORT_BY_EXT[${EXTENSION}]}
@ -45,7 +54,8 @@ if [ $ARCH == $HOST_ARCH -a $VM == 0 ]; then
SUDO="sudo"
OSBUILD="sudo osbuild"
else
echo "I don't support building for non-x86 nor in a VM."
SUDO=
OSBUILD="osbuildvm/osbuildvm --arch=$ARCH"
fi
EXPORT_ARGS="--export $EXPORT"