Skip to content
Snippets Groups Projects
libvirt-executor 4.68 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env bash
    set -o nounset -o errexit -o pipefail
    readonly MIRROR="https://mirror.pkgbuild.com"
    readonly LIBVIRT_DEFAULT_POOL_PATH="/var/lib/libvirt/images"
    readonly STATE_DIR="/usr/local/lib/libvirt-executor"
    
    ssh() {
      command ssh -i "${STATE_DIR}/id_rsa" -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=off "root@${vm_ip}" "${@}"
    }
    
    get_vm_ip() {
      if [[ -z "${vm_ip-}" ]]; then
        vm_ip="$(virsh -q domifaddr "${1}" | awk -F'[ /]+' '{print $5}')"
        [[ -n "${vm_ip}" ]] || return 1
      fi
    }
    
    get_vm_name() {
      printf 'libvirt_executor_runner_%s_project-%s_concurrent_%s\n' "${CUSTOM_ENV_CI_RUNNER_SHORT_TOKEN}" "${CUSTOM_ENV_CI_PROJECT_ID}" "${CUSTOM_ENV_CI_CONCURRENT_PROJECT_ID}"
    }
    
    clone_vm() {
      for _ in {1..10}; do
        # --reflink sadly doesn't work with non-raw formats:
        # https://bugzilla.redhat.com/show_bug.cgi?id=1324006
        if virt-clone -o "${1}" -n "${2}" --auto-clone; then
          return 0
        fi
        sleep 1
      done
      return 1
    }
    
    wait_for_ssh() {
      for _ in {1..90}; do
        if ! get_vm_ip "${1}"; then
          echo "Waiting for network"
          sleep 1
          continue
        fi
        if ! ssh true; then
          echo "Waiting for SSH to be ready"
          sleep 1
          continue
        fi
        return 0
      done
      echo 'Waited 90 seconds for VM to start, exiting...'
      exit "${SYSTEM_FAILURE_EXIT_CODE:-1}"
    }
    
    wait_for_vm_shutdown() {
      for _ in {1..10}; do
        if LC_ALL=C virsh domstate "${1}" | grep -F "shut off"; then
          return 0
        fi
        sleep 1
      done
      return 1
    }
    
    # Create a updated VM image with the required tools
    create_vm_template() {
      local vm_name
      printf -v vm_name 'libvirt_executor_vm_template_%(%s)T_tmp'
    
      local latest_image="$(curl -fs "${MIRROR}/images/latest/" | grep -Eo 'Arch-Linux-x86_64-cloudimg-[0-9]{8}\.[0-9]+\.qcow2'| head -n 1)"
      if [ -z "${latest_image}" ]; then
        echo "Error: Couldn't find latest cloud image"
        exit 1
      fi
      local image_path="${LIBVIRT_DEFAULT_POOL_PATH}/${vm_name}.qcow2"
      trap 'rm -f -- "${image_path}"' EXIT
      curl -sSf "${MIRROR}/images/latest/${latest_image}" --output "${image_path}"
      qemu-img resize "${image_path}" 10G
      local tmp_user_data
      tmp_user_data="$(mktemp -u)"
      trap 'rm -f -- "$tmp_user_data"; virsh destroy "${vm_name}"; virsh undefine "${vm_name}" --remove-all-storage; exit 1' EXIT
      sed "s:PUBLIC_SSH_KEY:$(<"${STATE_DIR}/id_rsa.pub"):" "${STATE_DIR}/user-data" > "${tmp_user_data}"
      virt-install --name "${vm_name}" \
                   --cloud-init "user-data=${tmp_user_data}" \
                   --disk path="${image_path}",device=disk \
                   --memory 1024 \
                   --vcpus 4 \
                   --os-type Linux \
                   --os-variant archlinux \
                   --network network=default,filterref.filter=clean-traffic \
                   --noautoconsole
      rm -- "${tmp_user_data}"
      wait_for_ssh "${vm_name}"
    
      ssh "cat > /etc/pacman.d/mirrorlist" <<< "Server = ${MIRROR}/\$repo/os/\$arch"
      ssh "cat > /etc/systemd/network/20-wired.network" <<< $'[Match]\nName=eth0\n[Network]\nDHCP=yes'
      ssh pacman -Sy --noconfirm --needed archlinux-keyring
      ssh pacman -Syu --noconfirm git git-lfs gitlab-runner
      ssh "sed -E 's/^#(IgnorePkg *=)/\1 linux/' -i /etc/pacman.conf"
    
      # Reboot to be sure the network is working
      virsh shutdown "${vm_name}"
      wait_for_vm_shutdown "${vm_name}"
      virsh start "${vm_name}"
      vm_ip=""
      wait_for_ssh "${vm_name}"
      ssh rm /etc/machine-id /var/lib/dbus/machine-id
    
      virsh shutdown "${vm_name}"
      wait_for_vm_shutdown "${vm_name}"
      virsh domrename "${vm_name}" "${vm_name%%_tmp}"
      trap - EXIT
    
      # Keep the 3 most recent VM templates
      virsh list --state-shutoff --name | grep "^libvirt_executor_vm_template_[0-9]*$" | sort -r | tail -n +4 | xargs -n 1 --no-run-if-empty virsh undefine --remove-all-storage
    }
    
    # https://docs.gitlab.com/runner/executors/custom.html#prepare
    prepare() {
      vm_template="$(virsh list --state-shutoff --name | grep "^libvirt_executor_vm_template_[0-9]*$" | sort -r | head -n 1)"
      if [[ -z "${vm_template}" ]]; then
        echo "Error no VM template found"
        exit 1
      fi
      vm_name="$(get_vm_name)"
      clone_vm "${vm_template}" "${vm_name}"
      virsh start "${vm_name}"
      wait_for_ssh "${vm_name}"
    }
    
    # https://docs.gitlab.com/runner/executors/custom.html#run
    run() {
      vm_name="$(get_vm_name)"
      wait_for_ssh "${vm_name}"
    
      ssh bash < "${1}" || exit "${BUILD_FAILURE_EXIT_CODE:-1}"
    
    }
    
    # https://docs.gitlab.com/runner/executors/custom.html#cleanup
    cleanup() {
      vm_name="$(get_vm_name)"
      virsh destroy "${vm_name}" || true
      virsh undefine "${vm_name}" --remove-all-storage
    }
    
    case "${1:-}" in
      create-vm-template)
        create_vm_template
        ;;
      prepare)
        prepare
        ;;
      run)
        run "${2}" "${3}"
        ;;
      cleanup)
        cleanup
        ;;
      *)
        echo "Error invalid command: ${1:-}"
        exit 1;
    esac