diff --git a/README.md b/README.md index ca58575..1b556a2 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,20 @@ running. $ vmctl -c CONFIG ``` -4. Prepare a boot image. The base configruation `*-base.conf` will look for a - base image in `img/base.qcow2`. You can use [archbase][archbase] to build a - lean Arch Linux base image or grab a QCOW2-based [Ubuntu cloud - image][ubuntu-cloud-image] if that's your vice. +4. Pre-run prep: + `vmctl` supports two methods: + * Imageless: Requires nix infrastructure and is based on creating the + system through the `nix build` command. Goto [Prep nix](#Prep-nix) before + running. + * Image-Backed: This is the "usual" way of running QEMU where the OS is in + images (qcow) on your filesystem. Goto [Prep boot img](#Prep-boot-img) + before running. - In the case of a standard "cloud image", you probably want to resize it - since it is usually shrunk to be as small as possible by default. - - $ qemu-img resize img/base.qcow2 8G - - **Note** The example `nvme.conf` will define `GUEST_BOOT="img/nvme.qcow2"`. - You do not need to provide that image - if it is not there `$GUEST_BOOT` - will be a differential image backed by `img/base.qcow2`. So, if you ever - need to reset to the "base" state, just remove the `img/nvme.qcow2` image. 5. Start from an example and edit it as you see fit. $ edit $HOME/vms/nvme.conf -[archbase]: https://github.com/OpenMPDK/archbase -[ubuntu-cloud-image]: https://cloud-images.ubuntu.com ## Virtual Machine Configurations @@ -74,35 +67,6 @@ the `CONFIG` config file in interactive mode such that the VM serial output is sent to standard out. The QEMU monitor is multiplexed to standard out, so you can access it by issuing `Ctrl-a c`. -### cloud-init - -If your chosen base image is meant to be configured through [cloud-init][cloud-init], -you can use the included cloud-config helper script to generate a basic -cloud-init seed image: - - $ ./contrib/generate-cloud-config-seed.sh ~/.ssh/id_rsa.pub - -If the image is running freebsd, use the script with `-freebsd` suffix: - - $ ./contrib/generate-cloud-config-seed-freebsd.sh ~/.ssh/id_rsa.pub - -This will generate a simple cloud-init seed image that will set up the image -with a default `vmuser` account that can be logged into using the given public -key. Place the output image (`seed.img`) in `img/` and pass the `--cloud-init` -(short: `'-c'`) option to `vmctl run` to initialize the image on first boot: - - $ vmctl -c CONFIG run -c - -cloud-init will automatically power off the virtual machine when it has been -configured. - -NOTE: For the cloud-config helper script to work `cloud-utils` is required. - -[cloud-init]: https://cloudinit.readthedocs.io/en/latest/ - - -### SSH, Serial console and QEMU monitor - By default, `vmctl` will launch the guest such that the serial console and the QEMU monitor is multiplexed to standard out. This means that you will see the serial console output directly on the screen. @@ -117,6 +81,7 @@ the console and monitor using $ vmctl -c CONFIG console $ vmctl -c CONFIG monitor + ### Tracing The `--trace` (short: `-t`) option can be used to enable tracing inside QEMU. @@ -130,6 +95,7 @@ to IRQs, use `vmctl` inserts an implicit `*`-suffix such that all traces with the given prefix is traced. + ### Custom kernel Finally, the `--kernel-dir` (short: `-k`) can be used to point to a custom @@ -142,6 +108,73 @@ that configures the image to support this. In non-cloud-init settings, see `contrib/systemd` for a systemd service that should be usable on most distributions. +## Prep nix + +Imageless mode depends on nix commands and flake infrastrcture. If you are on +nixos, add the following text to your configuration file (usually located at +/etc/nixos/configuration.nix): + + ``` + nix = { + package = pkgs.nix; + extraOptions = '' + experimental-features = nix-command flakes + ''; + }; + ``` + +On the other hand, if you have installed nix on another distribution (like Debian), +add the following text to your configruation file (usually /etc/nix/nix.conf): + + experimental-features = nix-command flakes + + +## Prep boot img + +The base configruation `*-base.conf` will look for a base image in +`img/base.qcow2`. You can use [archbase][archbase] to build a lean Arch Linux +base image or grab a QCOW2-based [Ubuntu cloud image][ubuntu-cloud-image] if +that's your vice. + +In the case of a standard "cloud image", you probably want to resize it since +it is usually shrunk to be as small as possible by default. + + $ qemu-img resize img/base.qcow2 8G + +**Note** The example `nvme.conf` will define `GUEST_BOOT="img/nvme.qcow2"`. +You do not need to provide that image - if it is not there `$GUEST_BOOT` +will be a differential image backed by `img/base.qcow2`. So, if you ever +need to reset to the "base" state, just remove the `img/nvme.qcow2` image. + +[archbase]: https://github.com/OpenMPDK/archbase +[ubuntu-cloud-image]: https://cloud-images.ubuntu.com + +### cloud-init + +If your chosen base image is meant to be configured through [cloud-init][cloud-init], +you can use the included cloud-config helper script to generate a basic +cloud-init seed image: + + $ ./contrib/generate-cloud-config-seed.sh ~/.ssh/id_rsa.pub + +If the image is running freebsd, use the script with `-freebsd` suffix: + + $ ./contrib/generate-cloud-config-seed-freebsd.sh ~/.ssh/id_rsa.pub + +This will generate a simple cloud-init seed image that will set up the image +with a default `vmuser` account that can be logged into using the given public +key. Place the output image (`seed.img`) in `img/` and pass the `--cloud-init` +(short: `'-c'`) option to `vmctl run` to initialize the image on first boot: + + $ vmctl -c CONFIG run -c + +cloud-init will automatically power off the virtual machine when it has been +configured. + +**Note**: For the cloud-config helper script to work `cloud-utils` is required. + +[cloud-init]: https://cloudinit.readthedocs.io/en/latest/ + ## License diff --git a/cmd/run b/cmd/run index 97241fd..81b087a 100644 --- a/cmd/run +++ b/cmd/run @@ -102,16 +102,6 @@ _run() { _load_vm - if [[ ! -f "${VMROOT}/$GUEST_BOOT" && ! -v do_print ]]; then - if [[ ! -f "${VMROOT}/$GUEST_BOOT_BASE" ]]; then - _fatal 1 "base image '${VMROOT}/${GUEST_BOOT_BASE}' does not exist" - fi - - _log "creating boot image ('$GUEST_BOOT') from base ('$GUEST_BOOT_BASE')" - _log_named "qemu-img create" "$QEMU_IMG" create -f qcow2 -b "$(basename "$GUEST_BOOT_BASE")" \ - -F qcow2 "${VMIMG}/$(basename "$GUEST_BOOT")" "$GUEST_BOOT_SIZE" - fi - if [[ -v do_gdb && -v do_background ]]; then _fatal 1 "--gdb cannot be used with --background" fi @@ -155,13 +145,7 @@ _run() { QEMU_PARAMS+=("-pidfile" "${VMROOT}/run/${VMNAME}/pidfile") if [[ -v kernel_dir ]]; then - local cmdline_extra="" - if [[ -v cloud_init_seed ]]; then - cmdline_extra="ci.datasource=NoCloud" - fi - - qemu_kern_add_custom --kernel-dir "${kernel_dir}" \ - --cmdline-extra "${cmdline_extra}" + qemu_kern_add_custom --kernel-dir "${kernel_dir}" fi if [[ -v do_background ]]; then diff --git a/common/rc b/common/rc index f5391df..3222a5a 100644 --- a/common/rc +++ b/common/rc @@ -129,6 +129,14 @@ _require_program() { fi } +_require_path() { + local rpath="$1"; shift + + if [[ ! -d "${rpath}" ]]; then + _fatal 1 "Path not found (${rpath}). $*" + fi +} + _is_running() { if [[ -f "${VMRUN}/pidfile" ]]; then return 0 diff --git a/contrib/nix/Makefile b/contrib/nix/Makefile new file mode 100644 index 0000000..b71cf1f --- /dev/null +++ b/contrib/nix/Makefile @@ -0,0 +1,21 @@ +CC := $(shell command -v musl-clang 2>/dev/null || echo gcc) +CFLAGS := -Os -static -ffunction-sections -fdata-sections +LDFLAGS := -Wl,--gc-sections +BUILDDIR ?= . +SRC := vmctl_init.c +OUT := $(BUILDDIR)/init +IMG := $(BUILDDIR)/init.img + +.PHONY: clean + +$(OUT): $(SRC) + @echo "Compiler: $(CC)" + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +$(IMG): $(OUT) + find . -type f -name init -printf '%P\n' | cpio -ov -H newc > $(IMG) + +all: $(IMG) + +clean: + rm -f $(OUT) $(IMG) diff --git a/contrib/nix/vmctl_init.c b/contrib/nix/vmctl_init.c new file mode 100644 index 0000000..bdaffef --- /dev/null +++ b/contrib/nix/vmctl_init.c @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#define VMCTL_INIT_VERBOSITY 0 +#if VMCTL_INIT_VERBOSITY +#include +#include + +char const * const indent_str = " "; +static void info(unsigned indent, char const *fmt, ...) { + while(0 != indent--) + fprintf(stdout, "%s", indent_str); + + va_list args; + va_start(args, fmt); + vfprintf(stdout, fmt, args); + va_end(args); + + fprintf(stdout, "\n"); +} +static void prnt_args(int argc, char *argv[]) { + info(1, "Arguments :\n"); + info(2, "argc = %d\n", argc); + info(2, "argv:\n"); + for (int i = 0; i < argc; i++) + info(3, "argv[%d] = %s\n", i, argv[i]); +} + +#else +static inline void info(unsigned indent, char const *fmt, ...) {} +static inline void prnt_args(int argc, char *argv[]) {} +#endif + +struct ArgOpt{ + const char *arg_str; + char **arg_val; +}; + +void parse_args(int const argc, char *argv[], struct ArgOpt *opts, int const opts_count) { + for (int i = 1; i < argc; i++) { + for (int j = 0; j < opts_count; j++) { + if (strcmp(argv[i], opts[j].arg_str) == 0 && i + 1 < argc) { + *opts[j].arg_val = argv[i + 1]; + i++; + break; + } + } + } +} + +int ensure_dir(const char *path) { + if (mkdir(path, 0755) == -1 && errno != EEXIST) { + info(1, "Error: creating the path (%s)", path); + return -1; + } + return 0; +} + +int mount_nix_store() { + int ret; + // This must match the tag used in virtiofsd + const char *source = "nixstore"; + const char *target = "/nix/store"; + + if (ensure_dir("/nix") || ensure_dir("/nix/store")) + return 1; + + if (mount(source, target, "virtiofs", 0, "") == -1) { + info(1, "Error: mounting source (%s) at target (%s)", source, target); + return 1; + } + + info(1, "Mounted %s at %s\n", source, target); + + return 0; +} + +int main(int argc, char *argv[]) { + int ret; + char *init_path = NULL; + struct ArgOpt opts[] = { + {"--init-path", &init_path}, + }; + + info(1, "VMCTL image-less INIT started ... \n"); + prnt_args(argc, argv); + + parse_args(argc, argv, opts, ARRAY_SIZE(opts)); + if (init_path == NULL) { + info(1, "Error: missing --init-path arg\n"); + return -1; + } + + if(mount_nix_store()) + return 1; + + info(1, "Attempting to exec: %s\n", init_path); + execv(init_path, argv); + info(1, "Error: execv failed"); + + return -1; +} diff --git a/contrib/nix/x86_64-noimgnix-base.nix b/contrib/nix/x86_64-noimgnix-base.nix new file mode 100644 index 0000000..5a41e91 --- /dev/null +++ b/contrib/nix/x86_64-noimgnix-base.nix @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copied from https://github.com/metaspace/run-kernel +# Original from Andreas Hindborg + +{ + description = "A system expression for vmctl"; + inputs = { nixpkgs.url = "nixpkgs/nixos-24.11"; }; + + outputs = { nixpkgs, ... }: { + nixosConfigurations.vm = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + + ({ pkgs, lib, modulesPath, ... }: { + imports = [ (modulesPath + "/profiles/minimal.nix") ]; + options = { }; + config = { + # Silence nix warning + system.stateVersion = "24.11"; + + # Silence missing root warning + fileSystems."/" = lib.mkImageMediaOverride { + fsType = "tmpfs"; + options = [ "mode=0755" ]; + }; + + # Disable early boot remount + systemd.services.systemd-remount-fs.enable = lib.mkForce false; + + # Disable Grub + boot.loader.grub.enable = false; + + # Uncomment if kernel and initrd are unneeded and take too long + #boot.kernel.enable = false; + #boot.initrd.enable = false; + + networking.hostName = ""; + + # Ensure DNS resolv works + networking.networkmanager.enable = true; + networking.resolvconf.enable = true; + + # The system is static. + users.mutableUsers = false; + + # Empty password for root + users.users.root.initialHashedPassword = ""; + + # Log in root automatically + services.getty.autologinUser = "root"; + + # Disable the oom killer + systemd.oomd.enable = false; + + # Disable firewall + networking.firewall.enable = false; + + # The system cannot be rebuilt + nix.enable = false; + + # No logical volume management + services.lvm.enable = false; + + # Enable ssh and allow root login without password + services.sshd.enable = true; + services.openssh.settings.PermitRootLogin = "yes"; + services.openssh.settings.PermitEmptyPasswords = "yes"; + security.pam.services.sshd.allowNullPassword = true; + + environment.systemPackages = [ pkgs.coreutils pkgs.python3 pkgs.wget ]; + }; + }) + ]; + }; + }; +} diff --git a/examples/vm/nvme.conf b/examples/vm/nvme.conf index 495d4f5..c51db08 100644 --- a/examples/vm/nvme.conf +++ b/examples/vm/nvme.conf @@ -3,11 +3,21 @@ QEMU_SYSTEM_X86_64=$(/usr/bin/which qemu-system-x86_64) source "x86_64-q35-base.conf" +#source "x86_64-q35-noimgnix-base.conf" +#source "aarch64-virt-base.conf" _setup_nvme() { # setup basevm + + # For x86_64-q35-base.conf _setup_x86_64_q35_base + # Uncomment for x86_64-q35-noimgnix-base.conf + #_setup_x86_64_q35_noimgnix_base + + # Uncomment for aarch64-virt-base.conf + #_setup_aarch64_virt_base + # pcie root port qemu_pcie_add_root_port "pcie_root_port0" \ --chassis 1 --slot 0 diff --git a/examples/vm/x86_64-q35-noimgnix-base.conf b/examples/vm/x86_64-q35-noimgnix-base.conf new file mode 100644 index 0000000..966014f --- /dev/null +++ b/examples/vm/x86_64-q35-noimgnix-base.conf @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Q35 x86_64 nix imageless configuration +# +# Requires the `nix` command and the `/nix/store` directory +# +# Configuration variables +# +# GUEST_DISPLAY Set to '1' to enable graphical output +# GUEST_CPU CPU model (QEMU -cpu paramater, default: 'host') +# GUEST_SMP SMP configuration (QEMU -smp parameter, default: '4') +# GUEST_MEMORY Guest RAM size (QEMU -m parameter, default: '8G') +# GUEST_NET_USER_HOSTFWDS List of ports to forward when using user-level networking +# (format: "[tcp|udp]:[hostaddr]:hostport-[guestaddr]:guestport", +# default: "tcp::2222-:22") +# NIX_INIT_REBUILD Force re-build nix init image. rebuild only when == "yes" +# NIX_STORE_PATH Set it if your store is != than /nix/store +# NIX_FLAKE_PATH Set it if you want a != sys than the default +# NIX_SYS_BUILD_FORCE Set it to "yes" when you want to force rebuild + +if [[ -f "common.conf" ]]; then + source "common.conf" +fi + +: "${QEMU_SYSTEM_BINARY:="${QEMU_SYSTEM_X86_64}"}" +: "${NIX_STORE_PATH:="/nix/store"}" +: "${NIX_INIT_REBUILD:="no"}" +: "${NIX_FLAKE_PATH:="${BASEDIR}/contrib/nix/x86_64-noimgnix-base.nix"}" +: "${NIX_SYS_BUILD_FORCE:="no"}" +: "${GUEST_DISPLAY:="0"}" +: "${GUEST_CPU:="host"}" +: "${GUEST_SMP:="4"}" +: "${GUEST_MEMORY:="8G"}" +: "${GUEST_GDB_PORT:=""}" +: "${GUEST_NET_USER_HOSTFWDS:="tcp::${GUEST_SSH_PORT}-:22"}" +: "${GUEST_KERNEL_IMAGE:="arch/x86_64/boot/bzImage"}" +: "${GUEST_KERNEL_APPEND_EXTRA:="audit=0 earlyprintk=serial"}" +: "${GUEST_KERNEL_CONSOLE:="ttyS0,115200"}" + +_setup_x86_64_q35_noimgnix_base() { + + QEMU_PARAMS+=("-nodefaults") + + if [[ $GUEST_DISPLAY -eq 0 ]]; then + QEMU_PARAMS+=("-display" "none") + else + QEMU_PARAMS+=("-vga" "std") + fi + + QEMU_PARAMS+=("-machine" "q35,accel=kvm,kernel-irqchip=split") + QEMU_PARAMS+=("-cpu" "$GUEST_CPU") + QEMU_PARAMS+=("-smp" "$GUEST_SMP") + QEMU_PARAMS+=("-m" "$GUEST_MEMORY") + + if [[ -n $GUEST_GDB_PORT ]]; then + QEMU_PARAMS+=("-gdb" "tcp::$GUEST_GDB_PORT") + fi + + # simple user-level networking + QEMU_PARAMS+=("-netdev" "user,id=net0,hostfwd=${GUEST_NET_USER_HOSTFWDS}") + QEMU_PARAMS+=("-device" "virtio-net-pci,netdev=net0") + + # hw rng + QEMU_PARAMS+=("-device" "virtio-rng-pci") +} diff --git a/lib/qemu/kern b/lib/qemu/kern index 503aab7..333f012 100644 --- a/lib/qemu/kern +++ b/lib/qemu/kern @@ -4,13 +4,25 @@ # # Written by Joel Granados - -_qemu_kern_noimg_add() { - _fatal 1 "${FUNCNAME[0]} is not supported yet" -} - _qemu_kern_img_add() { local cmdline_extra="$1" + + for var in GUEST_BOOT GUEST_BOOT_BASE; do + if [[ ! -v ${var} ]]; then + _fatal 1 "${FUNCNAME[0]} Missing var ${var}. Add it to ${VMCONFIG}" + fi + done + + if [[ ! -f "${VMROOT}/$GUEST_BOOT" ]]; then + if [[ ! -f "${VMROOT}/$GUEST_BOOT_BASE" ]]; then + _fatal 1 "base image '${VMROOT}/${GUEST_BOOT_BASE}' does not exist" + fi + + _log "creating boot image ('$GUEST_BOOT') from base ('$GUEST_BOOT_BASE')" + _log_named "qemu-img create" "$QEMU_IMG" create -f qcow2 -b "$(basename "$GUEST_BOOT_BASE")" \ + -F qcow2 "${VMIMG}/$(basename "$GUEST_BOOT")" "$GUEST_BOOT_SIZE" + fi + if [[ ! -v GUEST_KERNEL_BOOTDEV ]]; then GUEST_KERNEL_BOOTDEV="/dev/vda1" fi @@ -67,7 +79,7 @@ qemu_kern_add_custom() { done if [[ -v NIX_STORE_PATH ]]; then - _qemu_kern_noimg_add + qemu_nix_add_noimgnix else _qemu_kern_img_add "${cmdline_extra}" fi diff --git a/lib/qemu/nix b/lib/qemu/nix new file mode 100644 index 0000000..c52c74f --- /dev/null +++ b/lib/qemu/nix @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Written by Joel Granados + +_qemu_nix_create_nix_sys () { + local dst_path="$1" + local flake_attr_path="nixosConfigurations.vm.config.system.build.toplevel" + local flake_dst_dir="${VMSTATE}" + local flake_dst_path="${flake_dst_dir}/flake.nix" + + : "${NIX_FLAKE_PATH:="${BASEDIR}/contrib/nix/x86_64-noimgnix-base.nix"}" + if [[ ! -f ${NIX_FLAKE_PATH} ]]; then + _fatal 1 "${FUNCNAME[0]}: File not found (${NIX_FLAKE_PATH})" + fi + + if ! cp -f "${NIX_FLAKE_PATH}" "${flake_dst_path}"; then + _fatal 1 "${FUNCNAME[0]}: Error copying \ + src:(${NIX_FLAKE_PATH}) dest:(${flake_dst_path})" + fi + + local nix_build_cmd="nix build \ + 'path:${flake_dst_dir}#${flake_attr_path}' \ + -o \"${dst_path}\" \ + --no-write-lock-file" + if ! eval "${nix_build_cmd}"; then + _fatal 1 "${FUNCNAME[0]}: Error running '${nix_build_cmd}'" + fi +} + +_qemu_nix_create_nix_init () { + local state_dir="$1" + local init_path="${state_dir}/init" + if [[ -f ${init_path} && -v NIX_INIT_REBUILD && ${NIX_INIT_REBUILD} != "yes" ]]; then + return + fi + + _require_program make + _require_program "$(command -v musl-clang 2> /dev/null || echo gcc)" + + local make_path="${state_dir}/Makefile" + if [[ ! -f ${make_path} ]]; then + if ! ln -s "${BASEDIR}/contrib/nix/Makefile" "${make_path}"; then + _fatal 1 "${FUNCNAME[0]}: Error creating sym link for ${make_path}" + fi + fi + + local c_init_path="${state_dir}/vmctl_init.c" + if [[ ! -f ${c_init_path} ]]; then + if ! ln -s "${BASEDIR}/contrib/nix/vmctl_init.c" "${c_init_path}"; then + _fatal 1 "${FUNCNAME[0]}: Error creating sym link for ${c_init_path}" + fi + fi + + if [[ ${NIX_INIT_REBUILD} == "yes" ]]; then + if ! make -C "${state_dir}" clean; then + _fatal 1 "${FUNCNAME[0]}: Error cleaning ${c_init_path}" + fi + fi + if ! make -C "${state_dir}" all; then + _fatal 1 "${FUNCNAME[0]}: Error compiling ${c_init_path}" + fi +} + +qemu_nix_add_noimgnix () { + _require_program nix + _require_path "${NIX_STORE_PATH}" + + local long="cmd-line-extra:" + + if ! tmp=$(getopt -o "" --long "$long" -n "${FUNCNAME[0]}" -- "$@"); then + exit 1 + fi + + eval set -- "$tmp" + unset tmp + + local cmd_line_extra="" + while true; do + case "$1" in + '--cmd-line-extra' ) + cmd_line_extra="$2"; shift 2 + ;; + '--' ) + shift; break + ;; + * ) + _fatal 1 "unknown argument '$1'" + ;; + esac + done + + if [[ ! -v VMSTATE ]]; then + _fatal 1 "${FUNCNAME[0]}: VMSTATE global var is not set" + fi + + local dst_path="${VMSTATE}/nixsys" + : "${NIX_SYS_BUILD_FORCE:="no"}" # defaults to "no" if not set + if [[ ${NIX_SYS_BUILD_FORCE} == "yes" || ! -h ${dst_path} ]]; then + _qemu_nix_create_nix_sys "${dst_path}" + fi + + _qemu_nix_create_nix_init "${VMSTATE}" + + local nix_init_path="" + nix_init_path="$(readlink "${dst_path}")/init" + local cmd_line="root=/dev/ram0 console=${GUEST_KERNEL_CONSOLE}" + if [[ -v GUEST_KERNEL_APPEND_EXTRA ]]; then + cmd_line="${cmd_line} ${GUEST_KERNEL_APPEND_EXTRA}" + fi + cmd_line="${cmd_line} ${cmd_line_extra} -- --init-path ${nix_init_path}" + + QEMU_PARAMS+=("-kernel" "${kernel_dir}/${GUEST_KERNEL_IMAGE}") + QEMU_PARAMS+=("-append" "${cmd_line}") + QEMU_PARAMS+=("-initrd" "${VMSTATE}/init.img") + + qemu_share_add --shared-dir "${NIX_STORE_PATH}" \ + --tag "nixstore" \ + --share-type "viofs" +}