From: Kienan Stewart Date: Tue, 31 Oct 2023 13:57:03 +0000 (-0400) Subject: jjb: Add jobs for building ci/dev images X-Git-Url: https://git.lttng.org./?a=commitdiff_plain;h=d329b32d356939fe858054e75d5390cb74596c93;p=lttng-ci.git jjb: Add jobs for building ci/dev images Change-Id: I68ec852b8dcf4775966b8bfb4a53e1d539b58d2b Signed-off-by: Kienan Stewart --- diff --git a/automation/images/.gitkeep b/automation/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/jobs/images.yml b/jobs/images.yml new file mode 100644 index 0000000..552cdb4 --- /dev/null +++ b/jobs/images.yml @@ -0,0 +1,258 @@ +--- +## Anchors +- _images_parameters_default: &images_parameters_imagebuilder_defaults + name: 'images_parameters_imagebuilder_defaults' + parameters: + - string: &images_parameters_OS + name: 'OS' + description: 'OS name' + default: 'debian' + required: true + - string: &images_parameters_RELEASE + name: 'RELEASE' + description: 'OS release number or name' + default: 'bookworm' + required: true + - choice: &images_parameters_ARCH + name: 'ARCH' + description: 'Target architecture' + choices: + - amd64 + - i386 + - arm64 + - armhf + - ppc64el + - s390x + - riscv64 + - string: &images_parameters_VARIANT + name: 'VARIANT' + default: 'cloud' + description: 'The base image variant to build off of' + required: true + - choice: &images_parameters_IMAGE_TYPE + name: 'IMAGE_TYPE' + choices: + - 'lxd' + - 'vm' + description: 'The type of image to create' + - choice: + name: 'PROFILE' + choices: + - 'ci-node' + - 'developer' + description: 'The ansible group to apply to the image' + required: true + - string: &images_parameters_LXD_HOST + name: 'LXD_HOST' + default: 'ci-host-amd64-1a.internal.efficios.com' + description: 'The address of the LXD cluster to publish to' + required: true + - string: &images_parameters_LXD_INSTANCE_PROFILE + name: 'LXD_INSTANCE_PROFILE' + default: 'ci-rootnode' + description: 'The LXD instance profile to use for temporary instances when building images' + required: true + - string: &images_parameters_GIT_URL + name: 'GIT_URL' + default: 'https://github.com/lttng/lttng-ci.git' + description: 'The source of the repo containing the ansible playbooks' + required: true + - string: &images_parameters_GIT_BRANCH + name: 'GIT_BRANCH' + default: 'master' + description: 'The branch or commit of the ansible playbook repo to checkout' + required: true + - bool: &images_parameters_TEST + name: 'TEST' + default: true + description: 'Enable to launch a container of the published image as a test' + +- _images_parameters_default: &images_parameters_distrobuilder_defaults + name: 'images_parameters_distrobuilder_defaults' + parameters: + - string: + <<: *images_parameters_OS + - string: + <<: *images_parameters_RELEASE + - choice: + <<: *images_parameters_ARCH + - string: + <<: *images_parameters_VARIANT + - choice: + <<: *images_parameters_IMAGE_TYPE + - string: + <<: *images_parameters_LXD_HOST + - string: + <<: *images_parameters_LXD_INSTANCE_PROFILE + - string: + <<: *images_parameters_GIT_URL + - string: + <<: *images_parameters_GIT_BRANCH + - bool: + <<: *images_parameters_TEST + - string: + name: 'DISTROBUILDER_GIT_URL' + default: 'https://github.com/lxc/distrobuilder.git' + - string: + name: 'DISTROBUILDER_GIT_BRANCH' + default: 'main' + - string: + name: 'LXC_CI_GIT_URL' + default: 'https://github.com/lxc/lxc-ci.git' + - string: + name: 'LXC_CI_GIT_BRANCH' + default: 'main' + - string: + name: 'GO_VERSION' + default: '1.21.3' + +- _images_properties_defaults: &images_properties_defaults + name: 'images_properties_defaults' + properties: + - build-discarder: + num-to-keep: 20 + - throttle: + option: project + max-total: 4 + matrix-builds: false + +- _images_parameters_debian_defaults: &images_parameters_debian_defaults + name: 'image_parameters_debian_defaults' + parameters: + - bool: + name: 'SKIP_BASE_IMAGES' + default: false + - bool: + name: 'SKIP_PROFILE_IMAGES' + default: false + - choice: &images_parameters_arch_filter + name: 'ARCH_FILTER' + choices: + - all + - amd64 + - i386 + - arm64 + - armhf + - ppc64el + - riscv64 + - s390x + - choice: &images_parameters_image_type_filter + name: 'IMAGE_TYPE_FILTER' + choices: + - all + - lxd + - vm + - choice: &images_parameters_profile_filter + name: 'PROFILE_FILTER' + choices: + - all + - ci-node + - developer + - choice: + name: 'RELEASE_FILTER' + choices: + - all + - bullseye + - bookworm + - trixie + - sid + - string: + <<: *images_parameters_GIT_URL + - string: + <<: *images_parameters_GIT_BRANCH + +## Defaults +- defaults: + name: imagebuilder + concurrent: true + description: | +

Job is managed by Jenkins Job Builder

+ project-type: freestyle + publishers: + - workspace-cleanup + wrappers: + - workspace-cleanup + - timestamps + - ansicolor + - credentials-binding: + - ssh-user-private-key: + credential-id: 'f3c907b6-7485-49e1-afe1-4df24fac4ca1' + key-file-variable: SSH_PRIVATE_KEY + username-variable: SSH_USERNAME + passphrase-variable: SSH_PASSWORD + - file: + credential-id: 'f3f08275-59ef-42ff-9de5-9beafc7435b8' + variable: LXD_CLIENT_CERT + - file: + credential-id: '0debf23b-191b-4cdf-8a25-04e9a7092a67' + variable: LXD_CLIENT_KEY + - inject: {} + +## Templates +- job-template: + name: images_imagebuilder_{OS} + defaults: imagebuilder + description: | + This pipeline starts distrobuilder and imagebuilder jobs for {OS} + +

Job is managed by Jenkins Job Builder

+ project-type: pipeline + <<: *images_parameters_debian_defaults + IMAGE_TYPES: + - lxd + - vm + PROFILES: + - ci-node + - developer + dsl: !include-jinja2: pipelines/images/default.groovy + +- job-template: + name: images_distrobuilder + defaults: imagebuilder + node: 'deb12-amd64-rootnode' + <<: *images_parameters_distrobuilder_defaults + <<: *images_properties_defaults + builders: + - shell: !include-raw-escape: pipelines/images/distrobuild.sh + +- job-template: + name: images_imagebuilder + defaults: imagebuilder + node: 'deb12-amd64-rootnode' + <<: *images_parameters_imagebuilder_defaults + <<: *images_properties_defaults + builders: + - shell: !include-raw-escape: pipelines/images/imagebuild.sh + + +## Views +- view-template: + name: 'Images' + view-type: list + regex: 'image.*' + +## Projects + +- project: + name: images_imagebuilder_OS + OS: + - debian + ARCHES: + - i386 + - amd64 + RELEASES: + - bullseye + - bookworm + - trixie + - sid + jobs: + - 'images_imagebuilder_{OS}' +- project: + name: images_basejobs + jobs: + - 'images_imagebuilder' + - 'images_distrobuilder' +- project: + name: images_imagebuilder_views + views: + - Images diff --git a/pipelines/images/default.groovy b/pipelines/images/default.groovy new file mode 100644 index 0000000..615c48f --- /dev/null +++ b/pipelines/images/default.groovy @@ -0,0 +1,88 @@ +#!groovy + +def OS = '{{OS}}' +def RELEASES = {{RELEASES}} +def ARCHES = {{ARCHES}} +def IMAGE_TYPES = {{IMAGE_TYPES}} +def PROFILES = {{PROFILES}} +def c = [RELEASES, + ARCHES, + IMAGE_TYPES].combinations() +c.removeAll({ + (params.ARCH_FILTER != 'all' && it[1] != params.ARCH_FILTER) || + (params.IMAGE_TYPE_FILTER != 'all' && it[2] != params.IMAGE_TYPE_FILTER) || + (params.RELEASE_FILTER != 'all' && it[0] != params.RELEASE_FILTER) +}) + +// Skip i386 Vms +c.removeAll({ + it[1] == 'i386' && it[2] == 'vm' +}) + +def base_image_tasks = [:] +def profile_image_tasks = [:] +for(int index = 0; index < c.size(); index++) { + def envMap = [ + RELEASE: c[index][0], + ARCH: c[index][1], + IMAGE_TYPE: c[index][2] + ] + def image_name = "${OS}/${envMap.RELEASE}/${envMap.ARCH}/${envMap.IMAGE_TYPE}" + base_image_tasks[image_name] = { -> + def job_ids = [] + stage("base:${image_name}") { + print(envMap) + build( + job: 'images_distrobuilder', + parameters: [ + string(name: 'OS', value: OS), + string(name: 'RELEASE', value: envMap.RELEASE), + string(name: 'ARCH', value: envMap.ARCH), + string(name: 'IMAGE_TYPE', value: envMap.IMAGE_TYPE), + string(name: 'GIT_URL', value: params.GIT_URL), + string(name: 'GIT_BRANCH', value: params.GIT_BRANCH) + ] + ) + } + } + for (int profile_index = 0; profile_index < PROFILES.size(); profile_index++) { + // Using a second map gets around some weirdness with the closures finding + // PROFILES[profile_index] where most jobs would have a null value for the + // profile + def envMap2 = envMap.clone() + envMap2.PROFILE = PROFILES[profile_index] + if (env.PROFILE_FILTER == 'all' || env.PROFILE_FILTER == PROFILES[profile_index]) { + profile_image_tasks["${PROFILES[profile_index]}:${image_name}"] = { -> + print(envMap2) + build( + job: 'images_imagebuilder', + parameters: [ + string(name: 'OS', value: OS), + string(name: 'RELEASE', value: envMap2.RELEASE), + string(name: 'ARCH', value: envMap2.ARCH), + string(name: 'IMAGE_TYPE', value: envMap2.IMAGE_TYPE), + string(name: 'PROFILE', value: envMap2.PROFILE), + string(name: 'GIT_URL', value: params.GIT_URL), + string(name: 'GIT_BRANCH', value: params.GIT_BRANCH) + ] + ) + } + } + } +} + +if (!params.SKIP_BASE_IMAGES) { + stage("base images") { + parallel(base_image_tasks) + } +} + +if (!params.SKIP_PROFILE_IMAGES) { + // While it's possible to have the tasks in "base images" start + // their respective profile images_imagebuilder steps, it ends + // up creating a pipeline overview and log that is difficult to + // read in the Jenkins interface. + stage("profile images") { + parallel(profile_image_tasks) + } +} diff --git a/pipelines/images/distrobuild.sh b/pipelines/images/distrobuild.sh new file mode 100644 index 0000000..b5e624f --- /dev/null +++ b/pipelines/images/distrobuild.sh @@ -0,0 +1,205 @@ +#!/usr/bin/bash -eux + +CLEANUP=() + +function cleanup { + set +e + for (( index=${#CLEANUP[@]}-1 ; index >= 0 ; index-- )) ;do + ${CLEANUP[$index]} + done + CLEANUP=() + set -e +} + +function fail { + CODE="${1:-1}" + REASON="${2:-Unknown reason}" + cleanup + echo "${REASON}" >&2 + exit "${CODE}" +} + +trap cleanup EXIT TERM INT + +env + +REQUIRED_VARIABLES=( + OS + RELEASE + ARCH + IMAGE_TYPE + VARIANT + GIT_BRANCH + GIT_URL + LXD_CLIENT_CERT + LXD_CLIENT_KEY + TEST + DISTROBUILDER_GIT_URL + DISTROBUILDER_GIT_BRANCH + LXC_CI_GIT_URL + LXC_CI_GIT_BRANCH + GO_VERSION +) +MISSING_VARS=0 +for var in "${REQUIRED_VARIABLES[@]}" ; do + if [ ! -v "$var" ] ; then + MISSING_VARS=1 + echo "Missing required variable: '${var}'" >&2 + fi +done +if [[ ! "${MISSING_VARS}" == "0" ]] ; then + fail 1 "Missing required variables" +fi + +# Optional variables +INSTANCE_START_TIMEOUT="${INSTANCE_START_TIMEOUT:-30}" +VM_ARG=() + +# Install lxd-client +apt-get update +apt-get install -y lxd-client +mkdir -p ~/.config/lxc +cp "${LXD_CLIENT_CERT}" ~/.config/lxc/client.crt +cp "${LXD_CLIENT_KEY}" ~/.config/lxc/client.key +CLEANUP+=( + "rm -f ${HOME}/.config/lxc/client.crt" + "rm -f ${HOME}/.config/lxc/client.key" +) +lxc remote add ci --accept-certificate --auth-type tls "${LXD_HOST}" +lxc remote switch ci + +# Exit gracefully if the lxc images: provides the base image +IMAGE_NAME="${OS}/${RELEASE}/${VARIANT}/${ARCH}" +TYPE_FILTER='type=container' +if [[ "${IMAGE_TYPE}" == "vm" ]] ; then + TYPE_FILTER='type=virtual-machine' +fi +if [[ "$(lxc image list -f csv images:"${IMAGE_NAME}" -- "${TYPE_FILTER}" | wc -l)" != "0" ]] ; then + echo "Image '${IMAGE_NAME}' provided by 'images:' remote" + exit 0 +fi + +# Get go +apt-get install -y wget +wget "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -O - | tar -C /usr/local -xzf - +export PATH="${PATH}:/usr/local/go/bin" + +# Install distrobuilder +apt-get install -y debootstrap rsync gpg squashfs-tools git \ + btrfs-progs dosfstools qemu-utils gdisk +cd "${WORKSPACE}" +git clone --branch="${DISTROBUILDER_GIT_BRANCH}" "${DISTROBUILDER_GIT_URL}" distrobuilder +cd distrobuilder +make +PATH="${PATH}:${HOME}/go/bin" + +# Get CI repo +cd "${WORKSPACE}" +git clone --branch="${GIT_BRANCH}" "${GIT_URL}" ci + +# Get the LXC CI repo +cd "${WORKSPACE}" +git clone --branch="${LXC_CI_GIT_BRANCH}" "${LXC_CI_GIT_URL}" lxc-ci + +IMAGE_DIRS=( + "${WORKSPACE}/ci/automation/images" + "${WORKSPACE}/lxc-ci/images" +) +EXTENSIONS=( + 'yml' + 'yaml' +) +IMAGE_FILE='' +for IMAGE_DIR in "${IMAGE_DIRS[@]}" ; do + for EXTENSION in "${EXTENSIONS[@]}" ; do + if [ -f "${IMAGE_DIR}/${OS}.${EXTENSION}" ] ; then + IMAGE_FILE="${IMAGE_DIR}/${OS}.${EXTENSION}" + break 2; + fi + done +done + +if [[ "${IMAGE_FILE}" == "" ]] ; then + fail 1 "Unable to find image file for '${OS}' in ${IMAGE_DIRS[@]}" +fi + +DISTROBUILDER_ARGS=( + distrobuilder + build-incus +) +if [[ "${IMAGE_TYPE}" == "vm" ]] ; then + DISTROBUILDER_ARGS+=('--vm') + VM_ARG=('--vm') +fi + +# This could be quite large, and /tmp may be a tmpfs backed +# by memory, so instead make it relative to the workspace directory +BUILD_DIR=$(mktemp -d -p "${WORKSPACE}") +CLEANUP+=( + "rm -rf ${BUILD_DIR}" +) +DISTROBUILDER_ARGS+=( + "${IMAGE_FILE}" + "${BUILD_DIR}" + '-o' + "image.architecture=${ARCH}" + '-o' + "image.variant=${VARIANT}" + '-o' + "image.release=${RELEASE}" + '-o' + "image.serial=$(date -u +%Y%m%dT%H:%M:%S%z)" +) + +# Run the build +${DISTROBUILDER_ARGS[@]} + +# Import +# As 'distrobuilder --import-into-incus=alias' doesn't work since it only +# connects to the local unix socket, and the remote instance cannot be specified +# at this time. +ROOTFS="${BUILD_DIR}/rootfs.squashfs" +if [[ "${IMAGE_TYPE}" == "vm" ]] ; then + ROOTFS="${BUILD_DIR}/disk.qcow2" +fi + +# Work-around for lxd not using qemu-system-i386: set the architecture to x86_64 +# which will use qemu-system-x86_64 and still run 32bit userspace/kernels fine. +if [[ "${ARCH}" == "i386" ]] ; then + TMP_DIR=$(mktemp -d) + pushd "${TMP_DIR}" + tar -xf "${BUILD_DIR}/incus.tar.xz" + sed -i 's/architecture: i386/architecture: x86_64/' metadata.yaml + tar -cf "${BUILD_DIR}/incus.tar.xz" ./* + popd + rm -rf "${TMP_DIR}" +fi + +lxc image import "${BUILD_DIR}/incus.tar.xz" "${ROOTFS}" --alias="${IMAGE_NAME}" ci: + +if [[ "${TEST}" == "true" ]] ; then + set +e + INSTANCE_NAME='' + if INSTANCE_NAME="$(lxc -q launch -e ${VM_ARG[@]} -p default -p "${LXD_INSTANCE_PROFILE}" "${IMAGE_NAME}")" ; then + INSTANCE_NAME="$(echo "${INSTANCE_NAME}" | cut -d':' -f2 | tr -d ' ')" + CLEANUP+=( + "lxc stop ${INSTANCE_NAME}" + ) + else + fail 1 "Failed to launch instance using image '${IMAGE_NAME}'" + fi + TIME_REMAINING="${INSTANCE_START_TIMEOUT}" + INSTANCE_STATUS='' + while true ; do + INSTANCE_STATUS="$(lxc exec "${INSTANCE_NAME}" hostname)" + if [[ "${INSTANCE_STATUS}" == "${INSTANCE_NAME}" ]] ; then + break + fi + sleep 1 + TIME_REMAINING=$((TIME_REMAINING - 1)) + if [ "${TIME_REMAINING}" -lt "0" ] ; then + fail 1 "Timed out waiting for instance to become available via 'lxc exec'" + fi + done + set -e +fi diff --git a/pipelines/images/imagebuild.sh b/pipelines/images/imagebuild.sh new file mode 100644 index 0000000..340f71c --- /dev/null +++ b/pipelines/images/imagebuild.sh @@ -0,0 +1,223 @@ +#!/usr/bin/bash -eux + +CLEANUP=() + +function cleanup { + set +e + for (( index=${#CLEANUP[@]}-1 ; index >= 0 ; index-- )) ;do + ${CLEANUP[$index]} + done + CLEANUP=() + set -e +} + +function fail { + CODE="${1:-1}" + REASON="${2:-Unknown reason}" + cleanup + echo "${REASON}" >&2 + exit "${CODE}" +} + +trap cleanup EXIT TERM INT + +env + +REQUIRED_VARIABLES=( + OS # OS name + RELEASE # OS release + ARCH # The image architecture + IMAGE_TYPE # The image type to create + VARIANT # The variant of the base image to use + PROFILE # The ansible group to apply to the new image + GIT_BRANCH # The git branch of the automation repo to checkout + GIT_URL # The git URL of the automation repo to checkout + LXD_CLIENT_CERT # Path to LXD client certificate + LXD_CLIENT_KEY # Path to LXD client certificate key + SSH_PRIVATE_KEY # Path to SSH private key + TEST # 'true' to test launching published image +) +MISSING_VARS=0 +for var in "${REQUIRED_VARIABLES[@]}" ; do + if [ ! -v "$var" ] ; then + MISSING_VARS=1 + echo "Missing required variable: '${var}'" >&2 + fi +done +if [[ ! "${MISSING_VARS}" == "0" ]] ; then + fail 1 "Missing required variables" +fi + +# Default optional variables +INSTANCE_START_TIMEOUT="${INSTANCE_START_TIMEOUT:-30}" +NETWORK_SLEEP="${NETWORK_SLEEP:-15}" + +# Dependencies +apt-get -y install lxd-client ansible jq + +# Configuration +mkdir -p ~/.config/lxc +cp "${LXD_CLIENT_CERT}" ~/.config/lxc/client.crt +cp "${LXD_CLIENT_KEY}" ~/.config/lxc/client.key +CLEANUP+=( + "rm -f ${HOME}/.config/lxc/client.crt" + "rm -f ${HOME}/.config/lxc/client.key" +) +lxc remote add ci --accept-certificate --auth-type tls "${LXD_HOST}" +lxc remote switch ci + +# Clone lttng-ci +git clone -b "${GIT_BRANCH}" "${GIT_URL}" ci +cd ci/automation/ansible || exit 1 + +SOURCE_IMAGE_NAME="${OS}/${RELEASE}/${VARIANT}/${ARCH}" +# Include IMAGE_TYPE since an alias may only be defined once even if the +# type of the image differs +TARGET_IMAGE_NAME="${OS}/${RELEASE}/${VARIANT}/${ARCH}/${PROFILE}/${IMAGE_TYPE}" +INSTANCE_NAME='' +# Try from local cache +VM_ARG=() +if [ "${IMAGE_TYPE}" == "vm" ] ; then + VM_ARG=("--vm") +fi + +set +e +# Test +# It's possible that concurrent image creation when running parallel jobs causes +# an error during the launch: +# Error: Failed instance creation: UNIQUE constraint failed: images.project_id, images.fingerprint +# C.f. https://github.com/canonical/lxd/issues/11636 +# +TRIES_MAX=3 +TRIES=0 +while [[ "${TRIES}" -lt "${TRIES_MAX}" ]] ; do + if ! INSTANCE_NAME=$(lxc -q launch -e "${VM_ARG[@]}" -p default -p "${LXD_INSTANCE_PROFILE}" "${SOURCE_IMAGE_NAME}") ; then + # Try from images + if ! INSTANCE_NAME=$(lxc -q launch -e "${VM_ARG[@]}" -p default -p "${LXD_INSTANCE_PROFILE}" images:"${SOURCE_IMAGE_NAME}") ; then + TRIES=$((TRIES + 1)) + echo "Failed to deployed ephemereal instance attempt ${TRIES}/${TRIES_MAX}" + if [[ "${TRIES}" -lt "${TRIES_MAX}" ]] ; then + continue + fi + fail 1 "Failed to deploy ephemereal instance" + else + break + fi + else + break + fi +done +INSTANCE_NAME="$(echo "${INSTANCE_NAME}" | cut -d ':' -f 2 | tr -d ' ')" +set -e + +CLEANUP+=( + "lxc stop ${INSTANCE_NAME}" +) + +# VMs may take more time to start, wait until instance is running +TIME_REMAINING="${INSTANCE_START_TIMEOUT}" +while true ; do + set +e + INSTANCE_STATUS=$(lxc exec "${INSTANCE_NAME}" hostname) + set -e + if [[ "${INSTANCE_STATUS}" == "${INSTANCE_NAME}" ]] ; then + break + fi + sleep 1 + TIME_REMAINING=$((TIME_REMAINING - 1)) + if [ "${TIME_REMAINING}" -lt "0" ] ; then + fail 1 "Timed out waiting for instance to become available via 'lxc exec'" + fi +done + +# Wait for cloud-init to finish +if [[ "${VARIANT}" == "cloud" ]] ; then + lxc exec "${INSTANCE_NAME}" -- cloud-init status -w +fi + +# Wait for instance to have an ip address (@TODO: is there a better approach?) +sleep "${NETWORK_SLEEP}" + +# @TODO: Handle case when iputils2 is not installed +INSTANCE_IP='' +POTENTIAL_INTERFACES=(eth0 enp5s0) +lxc exec "${INSTANCE_NAME}" -- ip a +set +e +for interface in "${POTENTIAL_INTERFACES[@]}" ; do + if ! DEV_INFO="$(lxc exec "${INSTANCE_NAME}" -- ip a show dev "${interface}")" ; then + continue + fi + INSTANCE_IP="$(echo "${DEV_INFO}" | grep -Eo 'inet [^ ]* ' | cut -d' ' -f2 | cut -d'/' -f1)" + if [[ "${INSTANCE_IP}" != "" ]] ; then + break + fi +done +set -e +if [[ "${INSTANCE_IP}" == "" ]] ; then + fail 1 "Failed to determine instance IP address" +fi + +ssh-keyscan "${INSTANCE_IP}" >> ~/.ssh/known_hosts2 +#lxc exec "${INSTANCE_NAME}" -- bash -c 'for i in /etc/ssh/ssh_host_*_key ; do ssh-keygen -l -f "$i" ; done' >> "${HOME}/.ssh/known_hosts" +CLEANUP+=( + "rm -f ${HOME}/.ssh/known_hosts2" +) +cp "${SSH_PRIVATE_KEY}" ~/.ssh/id_rsa +ssh-keygen -f ~/.ssh/id_rsa -y > ~/.ssh/id_rsa.pub +CLEANUP+=( + "rm -f ${HOME}/.ssh/id_rsa.pub" + "rm -f ${HOME}/.ssh/id_rsa" +) +lxc file push ~/.ssh/id_rsa.pub "ci:${INSTANCE_NAME}/root/.ssh/authorized_keys2" + +# Confirm working SSH connection +if ! ssh "${INSTANCE_IP}" hostname ; then + fail 1 "Unable to reach ephemereal instance over SSH" +fi + +# Run playbook +cat > fake-inventory <