diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2321625 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,79 @@ +name: Build VM Disk Image + +on: + push: + branches: '*' + tags: 'v*' + pull_request: + branches: + - master + +jobs: + build: + name: Build + runs-on: ubuntu-latest + container: ubuntu:21.04 + strategy: + matrix: + version: + - 6.8 + + architecture: + - x86-64 + - arm64 + + steps: + - name: Clone Repository + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Install Dependecies + run: apt update && apt install curl unzip "qemu-system-$QEMU_ARCHITECTURE" -y + env: + QEMU_ARCHITECTURE: ${{ + matrix.architecture == 'x86-64' && 'x86' || + matrix.architecture == 'arm64' && 'aarch64' || + matrix.architecture + }} + + - name: Install Packer + run: | + curl -o packer.zip -L https://releases.hashicorp.com/packer/1.7.1/packer_1.7.1_linux_amd64.zip + unzip packer.zip + mv packer /usr/local/bin + + - name: Copy UEFI + if: matrix.architecture == 'x86-64' + run: cp /usr/share/ovmf/OVMF.fd resources/ovmf.fd + + - name: Download QEMU UEFI + if: matrix.architecture == 'arm64' + run: curl -o resources/qemu_efi.fd -L http://releases.linaro.org/components/kernel/uefi-linaro/latest/release/qemu64/QEMU_EFI.fd + + # - name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + + - name: Build Image + run: | + PACKER_LOG=1 ./build.sh \ + '${{ matrix.version }}' \ + '${{ matrix.architecture }}' \ + -var 'headless=true' \ + -var 'readonly_boot_media=false' + + - name: Extract Version + id: version + if: startsWith(github.ref, 'refs/tags/v') + run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} + + - name: Create Release + id: create_release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v1 + with: + name: OpenBSD ${{ steps.version.outputs.VERSION }} + draft: true + files: output/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d5d224 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +resources/ovmf.fd +resources/qemu_efi.fd +packer_cache +packer_cache_backup +output diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..061d2fb --- /dev/null +++ b/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euxo pipefail + +OS_VERSION="$1"; shift +ARCHITECTURE="$1"; shift + +# rm -rf packer_cache + +packer build \ + -var-file "var_files/common.pkrvars.hcl" \ + -var-file "var_files/$ARCHITECTURE.pkrvars.hcl" \ + -var-file "var_files/$OS_VERSION/common.pkrvars.hcl" \ + -var-file "var_files/$OS_VERSION/$ARCHITECTURE.pkrvars.hcl" \ + "$@" \ + openbsd.pkr.hcl diff --git a/openbsd.pkr.hcl b/openbsd.pkr.hcl new file mode 100644 index 0000000..a85f82e --- /dev/null +++ b/openbsd.pkr.hcl @@ -0,0 +1,198 @@ +variable "os_version" { + type = string + description = "The version of the operating system to download and install" +} + +variable "architecture" { + default = "x86-64" + type = string + description = "The type of CPU to use when building" +} + +variable "machine_type" { + default = "pc" + type = string + description = "The type of machine to use when building" +} + +variable "cpu_type" { + default = "qemu64" + type = string + description = "The type of CPU to use when building" +} + +variable "memory" { + default = 4096 + type = number + description = "The amount of memory to use when building the VM in megabytes" +} + +variable "cpus" { + default = 2 + type = number + description = "The number of cpus to use when building the VM" +} + +variable "disk_size" { + default = "12G" + type = string + description = "The size in bytes of the hard disk of the VM" +} + +variable "checksum" { + type = string + description = "The checksum for the virtual hard drive file" +} + +variable "root_password" { + default = "vagrant" + type = string + description = "The password for the root user" +} + +variable "secondary_user_password" { + default = "vagrant" + type = string + description = "The password for the `secondary_user_username` user" +} + +variable "secondary_user_username" { + default = "vagrant" + type = string + description = "The name for the secondary user" +} + +variable "headless" { + default = false + description = "When this value is set to `true`, the machine will start without a console" +} + +variable "use_default_display" { + default = true + type = bool + description = "If true, do not pass a -display option to qemu, allowing it to choose the default" +} + +variable "display" { + default = "cocoa" + description = "What QEMU -display option to use" +} + +variable "accelerator" { + default = "tcg" + type = string + description = "The accelerator type to use when running the VM" +} + +variable "firmware" { + type = string + description = "The firmware file to be used by QEMU" +} + +variable "readonly_boot_media" { + default = true + description = "If true, the boot media will be mounted as readonly" +} + +variable "sudo_version" { + type = string + description = "The version of the sudo package to install" +} + +variable "rsync_version" { + type = string + description = "The version of the rsync package to install" +} + +locals { + image_architecture = var.architecture == "x86-64" ? "amd64" : var.architecture + image = "miniroot${replace(var.os_version, ".", "")}.img" + vm_name = "openbsd-${var.os_version}-${var.architecture}.qcow2" + + iso_target_extension = "img" + iso_target_path = "packer_cache" + iso_full_target_path = "${local.iso_target_path}/${sha1(var.checksum)}.${local.iso_target_extension}" + + qemu_architecture = var.architecture == "arm64" ? "aarch64" : ( + var.architecture == "x86-64" ? "x86_64" : var.architecture + ) + + readonly_boot_media = var.readonly_boot_media ? "on" : "off" +} + +source "qemu" "qemu" { + machine_type = var.machine_type + cpus = var.cpus + memory = var.memory + net_device = "e1000" + + disk_compression = true + disk_interface = "virtio" + disk_size = var.disk_size + format = "qcow2" + + headless = var.headless + use_default_display = var.use_default_display + display = var.display + accelerator = var.accelerator + qemu_binary = "qemu-system-${local.qemu_architecture}" + firmware = var.firmware + + boot_wait = "30s" + + boot_command = [ + "S", + "dhclient em0", + "ftp -o install.conf http://{{ .HTTPIP }}:{{ .HTTPPort }}/resources/install.conf", + "ftp -o install.sh http://{{ .HTTPIP }}:{{ .HTTPPort }}/resources/install.sh", + "SECONDARY_USER_USERNAME=${var.secondary_user_username} ", + "SECONDARY_USER_PASSWORD=${var.secondary_user_password} ", + "ROOT_PASSWORD=${var.root_password} ", + "DISKLABEL_TEMPLATE='http://{{ .HTTPIP }}:{{ .HTTPPort }}/resources/template.disklabel' ", + "sh install.sh && reboot" + ] + + ssh_username = "root" + ssh_password = var.root_password + ssh_timeout = "10000s" + + qemuargs = [ + ["-cpu", var.cpu_type], + ["-boot", "strict=off"], + ["-monitor", "none"], + ["-device", "virtio-scsi-pci"], + ["-device", "scsi-hd,drive=drive0,bootindex=0"], + ["-device", "scsi-hd,drive=drive1,bootindex=1"], + ["-drive", "if=none,file={{ .OutputDir }}/{{ .Name }},id=drive0,cache=writeback,discard=ignore,format=qcow2"], + ["-drive", "if=none,file=${local.iso_full_target_path},id=drive1,media=disk,format=raw,readonly=${local.readonly_boot_media}"], + ] + + iso_checksum = var.checksum + iso_target_extension = local.iso_target_extension + iso_target_path = local.iso_target_path + iso_urls = [ + "http://cdn.openbsd.org/pub/OpenBSD/${var.os_version}/${local.image_architecture}/${local.image}" + ] + + http_directory = "." + output_directory = "output" + shutdown_command = "shutdown -h -p now" + vm_name = local.vm_name +} + +build { + sources = ["qemu.qemu"] + + provisioner "shell" { + script = "resources/provision.sh" + environment_vars = [ + "SECONDARY_USER=${var.secondary_user_username}", + "SUDO_VERSION=${var.sudo_version}", + "RSYNC_VERSION=${var.rsync_version}" + ] + } + + /*provisioner "shell-local" { + inline = ["if [ -d packer_cache_backup ]; then cp packer_cache_backup/* ${local.iso_target_path}; fi"] + }*/ +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..475afd5 --- /dev/null +++ b/readme.md @@ -0,0 +1,74 @@ +# OpenBSD Builder + +This project builds the OpenBSD VM image for the +[cross-platform-actions/action](https://github.com/cross-platform-actions/action) +GitHub action. The image contains a standard OpenBSD installation without any +X components, man pages or games. It will install the following file sets: + +* bsd +* bsd.mp +* bsd.rd +* baseXX.tgz +* compXX.tgz + +In addition to the above file sets, the following packages are installed as well: + +* sudo +* bash +* curl +* rsync + +Except for the root user, there's one additional user, `runner`, which is the +user that will be running the commands in the GitHub action. This user is +allowed use `sudo` without a password. + +## Architectures and Versions + +The following architectures and versions are supported: + +| Version | x86-64 | arm64 | +|---------|--------|-------| +| 6.8 | ✓ | ✓ | + +## Building Locally + +### Prerequisite + +* [Packer](https://www.packer.io) 1.7.1 or later +* [QEMU](https://qemu.org) + +### Building + +1. Clone the repository: + ``` + git clone https://github.com/cross-platform-actions/openbsd-builder + cd openbsd-builder + ``` + +2. Run `build.sh` to build the image: + ``` + ./build.sh + ``` + Where `` and `` are the any of the versions or + architectures available in the above table. + +The above command will build the VM image and the resulting disk image will be +at the path: `output/openbsd-6.8-amd64.qcow2`. + +## Additional Information + +At startup, the image will look for a second hard drive. If present and it +contains a file named `keys` at the root, it will install this file as the +`authorized_keys` file for the `runner` user. The disk is expected to be +formatted as FAT32. This is used as an alternative to a shared folder between +the host and the guest, since this is not supported by the xhyve hypervisor. +FAT32 is chosen because it's the only filesystem that is supported by both the +host (macOS) and the guest (OpenBSD) out of the box. + +The VM needs to be configured with the `e1000` network device. The disk needs to +be configured with the GPT partitioning scheme. And the VM needs to be configured +to use UEFI. All this is required for the VM image to be able to run using the +xhyve hypervisor. + +The qcow2 format is chosen because unused space doesn't take up any space on +disk, it's compressible and easily converts the raw format, used by xhyve. diff --git a/resources/install.conf b/resources/install.conf new file mode 100644 index 0000000..b8d0f52 --- /dev/null +++ b/resources/install.conf @@ -0,0 +1,14 @@ +System hostname = openbsd +IPv4 address for = dhcp +Password for root account = {{ ROOT_PASSWORD }} +Do you expect to run the X Window System = no +Setup a user = {{ SECONDARY_USER_USERNAME }} +Password for user = {{ SECONDARY_USER_PASSWORD }} +Allow root ssh login = yes +What timezone are you in = Etc/UTC +Which disk is the root disk = sd0 +Use (W)hole disk MBR, whole disk (G)PT or (E)dit = g +URL to autopartitioning template for disklabel = {{ DISKLABEL_TEMPLATE }} +Http Server = cdn.openbsd.org +Set name(s) = -game* -x* -man* +Directory does not contain SHA256.sig. Continue without verification = yes diff --git a/resources/install.sh b/resources/install.sh new file mode 100755 index 0000000..122afce --- /dev/null +++ b/resources/install.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -exu + +sed -i'' "s/{{ SECONDARY_USER_USERNAME }}/$SECONDARY_USER_USERNAME/" /install.conf +sed -i'' "s/{{ SECONDARY_USER_PASSWORD }}/$SECONDARY_USER_PASSWORD/" /install.conf +sed -i'' "s/{{ ROOT_PASSWORD }}/$ROOT_PASSWORD/" /install.conf + +# Use # instead of / because the URL to the templates contains / +sed -i'' "s#{{ DISKLABEL_TEMPLATE }}#$DISKLABEL_TEMPLATE#" /install.conf + +# Always use the first line of ftplist.cgi for the default answer of "HTTP Server?". +# This is a workaround for the change introduced in the following commit: +# https://github.com/openbsd/src/commit/bf983825822b119e4047eb99486f18c58351f347 +sed -i'' 's/\[\[ -z $_l \]\] && //' /install.sub +/install -a -f /install.conf diff --git a/resources/provision.sh b/resources/provision.sh new file mode 100755 index 0000000..2275f6b --- /dev/null +++ b/resources/provision.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +set -exu + +install_extra_packages() { + pkg_add bash + pkg_add curl + pkg_add "rsync-$RSYNC_VERSION" +} + +setup_sudo() { + pkg_add "sudo-$SUDO_VERSION" + + cat < /etc/sudoers +#includedir /etc/sudoers.d +EOF + + mkdir -p /etc/sudoers.d + cat < "/etc/sudoers.d/$SECONDARY_USER" +Defaults:$SECONDARY_USER !requiretty +$SECONDARY_USER ALL=(ALL) NOPASSWD: ALL +EOF + + chmod 440 "/etc/sudoers.d/$SECONDARY_USER" +} + +configure_boot_scripts() { + cat <> /etc/rc.local +RESOURCES_MOUNT_PATH='/mnt/resources' + +mount_resources_disk() { + disk=\$(sysctl -n hw.disknames | sed 's/:[^,]*//g;s/,/ /' | cut -d ' ' -f 2 -s) + + if [ -n "\$disk" ]; then + partition=\$(disklabel \$disk | sed -n '/^ *[abd-z]: /s/^ *\([abd-z]\):.*/\1/p') + dev="/dev/\${disk}\${partition}" + mkdir -p /mnt/resources + mount_msdos "\$dev" /mnt/resources + fi +} + +install_authorized_keys() { + if [ -s "\$RESOURCES_MOUNT_PATH/KEYS" ]; then + mkdir -p "/home/$SECONDARY_USER/.ssh" + cp "\$RESOURCES_MOUNT_PATH/KEYS" "/home/$SECONDARY_USER/.ssh/authorized_keys" + chown "$SECONDARY_USER:$SECONDARY_USER" "/home/$SECONDARY_USER/.ssh/authorized_keys" + chmod 600 "/home/$SECONDARY_USER/.ssh/authorized_keys" + fi +} + +mount_resources_disk +install_authorized_keys +EOF +} + +configure_boot_flags() { + cat <> /etc/boot.conf +set tty com0 +set timeout 1 +EOF +} + +configure_ssh() { + cp /etc/ssh/sshd_config /tmp/sshd_config + sed '/^PermitRootLogin/s/ yes$/ no/' /tmp/sshd_config > /etc/ssh/sshd_config + rm /tmp/sshd_config + tee -a /etc/ssh/sshd_config <