|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +dn=$(dirname "$0") |
| 5 | +# shellcheck source=src/cmdlib.sh |
| 6 | +. "${dn}"/cmdlib.sh |
| 7 | + |
| 8 | + |
| 9 | +print_help() { |
| 10 | + cat 1>&2 <<EOF |
| 11 | +Usage: coreos-assembler buildextend-${platform} --help |
| 12 | + coreos-assembler buildextend-${platform} [--build ID] |
| 13 | +
|
| 14 | + Build a bare metal image. |
| 15 | +EOF |
| 16 | +} |
| 17 | + |
| 18 | + |
| 19 | +# Parse the passed config JSON and extract a mandatory value |
| 20 | +getconfig() { |
| 21 | + k=$1 |
| 22 | + config=$2 |
| 23 | + jq -re .\""$k"\" < "${config}" |
| 24 | +} |
| 25 | +# Return a configuration value, or default if not set |
| 26 | +getconfig_def() { |
| 27 | + k=$1 |
| 28 | + shift |
| 29 | + default=$1 |
| 30 | + config=$2 |
| 31 | + jq -re .\""$k"\"//\""${default}"\" < "${config}" |
| 32 | +} |
| 33 | + |
| 34 | +# Store information about the artifact into meta.json and also |
| 35 | +# "finalize" it, copying it into the builddir. |
| 36 | +postprocess_artifact() { |
| 37 | + local artifact_name=$1 |
| 38 | + local local_filepath=$2 |
| 39 | + local target_filename=$3 |
| 40 | + local skip_compress=$4 |
| 41 | + if [[ ! "${skip_compress}" =~ ^(True|False)$ ]]; then |
| 42 | + fatal "Must specify 'True' or 'False' for skip_compress. Provided: '${skip_compress}'" |
| 43 | + fi |
| 44 | + cosa meta --workdir "${workdir}" --build "${build}" --dump | python3 -c " |
| 45 | +import sys, json |
| 46 | +j = json.load(sys.stdin) |
| 47 | +j['images']['${artifact_name}'] = { |
| 48 | + 'path': '${target_filename}', |
| 49 | + 'sha256': '$(sha256sum_str < "${local_filepath}")', |
| 50 | + 'size': $(stat -c '%s' "${local_filepath}") |
| 51 | +} |
| 52 | +if ${skip_compress}: |
| 53 | + j['images']['${artifact_name}']['skip-compression'] = True |
| 54 | +json.dump(j, sys.stdout, indent=4) |
| 55 | +" > meta.json.new |
| 56 | + cosa meta --workdir "${workdir}" --build "${build}" --artifact-json meta.json.new |
| 57 | + /usr/lib/coreos-assembler/finalize-artifact "${local_filepath}" "${builddir}/${target_filename}" |
| 58 | + echo "Successfully generated: ${target_filename}" |
| 59 | +} |
| 60 | + |
| 61 | +# For qemu-secex we need to do a few extra things like spin up a |
| 62 | +# VM to run genprotimg and save off the pubkey for Ignition. |
| 63 | +postprocess_qemu_secex() { |
| 64 | + if [ ! -f "${genprotimgvm}" ]; then |
| 65 | + fatal "No genprotimgvm provided at ${genprotimgvm}" |
| 66 | + fi |
| 67 | + |
| 68 | + # Basic qemu args: |
| 69 | + qemu_args=(); blk_size="512" |
| 70 | + [[ $platform == metal4k ]] && blk_size="4096" |
| 71 | + qemu_args+=("-drive" "if=none,id=target,format=qcow,file=${imgpath},cache=unsafe" \ |
| 72 | + "-device" "virtio-blk,serial=target,drive=target,physical_block_size=${blk_size},logical_block_size=${blk_size}") |
| 73 | + |
| 74 | + # SecureVM (holding Universal Key for all IBM Z Mainframes) requires scripts to execute genprotimg |
| 75 | + se_script_dir="/usr/lib/coreos-assembler/secex-genprotimgvm-scripts" |
| 76 | + genprotimg_img="${PWD}/secex-genprotimg.img" |
| 77 | + genprotimg_dir=$(mktemp -p "${tmp_builddir}" -d) |
| 78 | + cp "${se_script_dir}/genprotimg-script.sh" "${se_script_dir}/post-script.sh" "${genprotimg_dir}" |
| 79 | + # Extra kargs with dm-verity hashes |
| 80 | + secex_kargs="ignition.firstboot" |
| 81 | + secex_kargs+=" rootfs.roothash=$(<"${outdir}/${platform}/rootfs_hash")" |
| 82 | + secex_kargs+=" bootfs.roothash=$(<"${outdir}/${platform}/bootfs_hash")" |
| 83 | + echo "${secex_kargs}" > "${genprotimg_dir}/parmfile" |
| 84 | + virt-make-fs --format=raw --type=ext4 "${genprotimg_dir}" "${genprotimg_img}" |
| 85 | + rm -rf "${genprotimg_dir}" |
| 86 | + qemu_args+=("-drive" "if=none,id=genprotimg,format=raw,file=${genprotimg_img}" \ |
| 87 | + "-device" "virtio-blk,serial=genprotimg,drive=genprotimg") |
| 88 | + |
| 89 | + # GPG keys used for protecting Ignition config |
| 90 | + tmp_gpg_home=$(mktemp -p "${tmp_builddir}" -d) |
| 91 | + ignition_pubkey=$(mktemp -p "${tmp_builddir}") |
| 92 | + ignition_prikey=$(mktemp -p "${tmp_builddir}") |
| 93 | + gpg --homedir "${tmp_gpg_home}" --batch --passphrase '' --yes --quick-gen-key "Secure Execution (secex) ${build}" rsa4096 encr none |
| 94 | + gpg --homedir "${tmp_gpg_home}" --armor --export secex > "${ignition_pubkey}" |
| 95 | + gpg --homedir "${tmp_gpg_home}" --armor --export-secret-key secex > "${ignition_prikey}" |
| 96 | + exec 9<"${ignition_prikey}" |
| 97 | + rm -rf "${tmp_gpg_home}" "${ignition_prikey}" |
| 98 | + qemu_args+=("-add-fd" "fd=9,set=3" "-drive" "if=none,id=gpgkey,format=raw,file=/dev/fdset/3,readonly=on" \ |
| 99 | + "-device" "virtio-blk,serial=gpgkey,drive=gpgkey") |
| 100 | + |
| 101 | + /usr/lib/coreos-assembler/secex-genprotimgvm-scripts/runvm.sh \ |
| 102 | + --genprotimgvm "${genprotimgvm}" -- "${qemu_args[@]}" |
| 103 | + rm -f "${genprotimg_img}" |
| 104 | + exec 9>&- |
| 105 | + |
| 106 | + # Now store the generated ${ignition_pubkey} in the builddir and meta.json |
| 107 | + gpg_key_filename="${name}-${build}-ignition-secex-key.gpg.pub" |
| 108 | + postprocess_artifact "ignition-gpg-key" "${ignition_pubkey}" "${gpg_key_filename}" 'True' |
| 109 | +} |
| 110 | + |
| 111 | +# Here we generate the input JSON we pass to runvm_osbuild for all of our image builds |
| 112 | +generate_runvm_osbuild_config() { |
| 113 | + runvm_osbuild_config_json="${workdir}/tmp/runvm-osbuild-config-${build}.json" |
| 114 | + echo "${runvm_osbuild_config_json}" # Let the caller know where the config will be |
| 115 | + if [ -f "${runvm_osbuild_config_json}" ]; then |
| 116 | + return # only need to generate this once per build |
| 117 | + fi |
| 118 | + rm -f "${workdir}"/tmp/runvm-osbuild-config-*.json # clean up any previous configs |
| 119 | + |
| 120 | + # reread these values from the build itself rather than rely on the ones loaded |
| 121 | + # by prepare_build since the config might've changed since then |
| 122 | + ostree_commit=$(meta_key ostree-commit) |
| 123 | + ostree_ref=$(meta_key ref) |
| 124 | + if [ "${ostree_ref}" = "None" ]; then |
| 125 | + ostree_ref="" |
| 126 | + fi |
| 127 | + |
| 128 | + ostree_repo=${tmprepo} |
| 129 | + # Ensure that we have the cached unpacked commit |
| 130 | + import_ostree_commit_for_build "${build}" |
| 131 | + # Note this overwrote the bits generated in prepare_build |
| 132 | + # for image_json. In the future we expect to split prepare_build |
| 133 | + # into prepare_ostree_build and prepare_diskimage_build; the |
| 134 | + # latter path would only run this. |
| 135 | + image_json=${workdir}/tmp/image.json |
| 136 | + |
| 137 | + # Grab a few values from $image_json |
| 138 | + deploy_via_container=$(getconfig_def "deploy-via-container" "" "${image_json}") |
| 139 | + extra_kargs="$(python3 -c 'import sys, json; args = json.load(sys.stdin)["extra-kargs"]; print(" ".join(args))' < "${image_json}")" |
| 140 | + |
| 141 | + # OStree container ociarchive file path |
| 142 | + ostree_container="${builddir}/$(meta_key images.ostree.path)" |
| 143 | + # If no container_imgref was set let's just set it to some professional |
| 144 | + # looking default. The name of the ociarchive file should suffice. |
| 145 | + container_imgref_default="ostree-image-signed:oci-archive:/$(basename "${ostree_container}")" |
| 146 | + container_imgref=$(getconfig_def "container_imgref" "${container_imgref_default}" "${image_json}") |
| 147 | + |
| 148 | + echo "Estimating disk size..." >&2 |
| 149 | + # The additional 35% here is obviously a hack, but we can't easily completely fill the filesystem, |
| 150 | + # and doing so has apparently negative performance implications. |
| 151 | + ostree_size_json="$(/usr/lib/coreos-assembler/estimate-commit-disk-size --repo "$ostree_repo" "$ostree_commit" --add-percent 35)" |
| 152 | + rootfs_size_mb="$(jq '."estimate-mb".final' <<< "${ostree_size_json}")" |
| 153 | + # The minimum size of a disk image we'll need will be the rootfs_size |
| 154 | + # estimate plus the size of the non-root partitions. We'll use this |
| 155 | + # size for the metal images, but for the IaaS/virt image we'll use |
| 156 | + # the size set in the configs since some of them have minimum sizes that |
| 157 | + # the platforms require and we want a "default" disk size that has some |
| 158 | + # free space. |
| 159 | + nonroot_partition_sizes=513 |
| 160 | + # On s390x there is one more build - Secure Execution case, which has |
| 161 | + # different image layout. We add the sizes of the se and verity |
| 162 | + # partitions so that they don't "eat into" the 35% buffer (though note |
| 163 | + # this is all blown away on first boot anyway). For 's390x.mpp.yaml' |
| 164 | + # simplicity all s390x images have same size (of secex image). |
| 165 | + if [[ $basearch == "s390x" ]]; then |
| 166 | + nonroot_partition_sizes=$((nonroot_partition_sizes + 200 + 128 + 256 + 1)) |
| 167 | + fi |
| 168 | + metal_image_size_mb="$(( rootfs_size_mb + nonroot_partition_sizes ))" |
| 169 | + cloud_image_size_mb="$(jq -r ".size*1024" < "${image_json}")" |
| 170 | + echo "Disk sizes: metal: ${metal_image_size_mb}M (estimated), cloud: ${cloud_image_size_mb}M" >&2 |
| 171 | + |
| 172 | + # Generate the JSON describing the disk we want to build |
| 173 | + yaml2json /dev/stdin "${runvm_osbuild_config_json}" <<EOF |
| 174 | +container-imgref: "${container_imgref}" |
| 175 | +deploy-via-container: "${deploy_via_container}" |
| 176 | +osname: "${name}" |
| 177 | +ostree-container: "${ostree_container}" |
| 178 | +ostree-ref: "${ref}" |
| 179 | +extra-kargs-string: "${extra_kargs}" |
| 180 | +ostree-repo: "${ostree_repo}" |
| 181 | +metal-image-size: "${metal_image_size_mb}" |
| 182 | +cloud-image-size: "${cloud_image_size_mb}" |
| 183 | +# Note: this is only used in the secex case; there, the rootfs is |
| 184 | +# not the last partition on the disk so we need to explicitly size it |
| 185 | +rootfs-size: "${rootfs_size_mb}" |
| 186 | +EOF |
| 187 | +} |
| 188 | + |
| 189 | + |
| 190 | +main() { |
| 191 | + # Set Some Defaults |
| 192 | + genprotimgvm=/data.secex/genprotimgvm.qcow2 |
| 193 | + build= |
| 194 | + force= |
| 195 | + |
| 196 | + # This script is used for creating several artifacts. For example, |
| 197 | + # `cmd-buildextend-qemu` is a symlink to `cmd-buildextend-metal`. |
| 198 | + case "$(basename "$0")" in |
| 199 | + "cmd-buildextend-metal") platform=metal;; |
| 200 | + "cmd-buildextend-metal4k") platform=metal4k;; |
| 201 | + "cmd-buildextend-qemu") platform=qemu;; |
| 202 | + "cmd-buildextend-qemu-secex") platform=qemu-secex;; |
| 203 | + "cmd-buildextend-secex") platform=qemu-secex;; |
| 204 | + *) fatal "called as unexpected name $0";; |
| 205 | + esac |
| 206 | + |
| 207 | + options=$(getopt --options h --longoptions help,force,build:,genprotimgvm: -- "$@") || { |
| 208 | + print_help |
| 209 | + exit 1 |
| 210 | + } |
| 211 | + eval set -- "$options" |
| 212 | + while true; do |
| 213 | + case "$1" in |
| 214 | + -h | --help) |
| 215 | + print_help |
| 216 | + exit 0 |
| 217 | + ;; |
| 218 | + --force) |
| 219 | + force=1 |
| 220 | + ;; |
| 221 | + --build) |
| 222 | + build=$2 |
| 223 | + shift |
| 224 | + ;; |
| 225 | + --genprotimgvm) |
| 226 | + genprotimgvm="$2" |
| 227 | + shift |
| 228 | + ;; |
| 229 | + --) |
| 230 | + shift |
| 231 | + break |
| 232 | + ;; |
| 233 | + -*) |
| 234 | + fatal "$0: unrecognized option: $1" |
| 235 | + ;; |
| 236 | + *) |
| 237 | + break |
| 238 | + ;; |
| 239 | + esac |
| 240 | + shift |
| 241 | + done |
| 242 | + |
| 243 | + if [ $# -ne 0 ]; then |
| 244 | + print_help |
| 245 | + fatal "Too many arguments passed" |
| 246 | + fi |
| 247 | + |
| 248 | + case "$basearch" in |
| 249 | + "x86_64"|"aarch64"|"s390x"|"ppc64le") ;; |
| 250 | + *) fatal "$basearch is not supported for this command" ;; |
| 251 | + esac |
| 252 | + |
| 253 | + # shellcheck disable=SC2031 |
| 254 | + export LIBGUESTFS_BACKEND=direct |
| 255 | + export IMAGE_TYPE="${platform}" |
| 256 | + prepare_build |
| 257 | + |
| 258 | + if [ -z "${build}" ]; then |
| 259 | + build=$(get_latest_build) |
| 260 | + if [ -z "${build}" ]; then |
| 261 | + fatal "No build found." |
| 262 | + fi |
| 263 | + fi |
| 264 | + |
| 265 | + builddir=$(get_build_dir "$build") |
| 266 | + if [ ! -d "${builddir}" ]; then |
| 267 | + fatal "Build dir ${builddir} does not exist." |
| 268 | + fi |
| 269 | + |
| 270 | + # add building sempahore |
| 271 | + build_semaphore="${builddir}/.${platform}.building" |
| 272 | + if [ -e "${build_semaphore}" ]; then |
| 273 | + fatal "${build_semaphore} found: another process is building ${platform}" |
| 274 | + fi |
| 275 | + touch "${build_semaphore}" |
| 276 | + trap 'rm -f ${build_semaphore}' EXIT |
| 277 | + |
| 278 | + # check if the image already exists in the meta.json |
| 279 | + if [ -z "${force}" ]; then |
| 280 | + meta_img=$(meta_key "images.${platform}.path") |
| 281 | + if [ "${meta_img}" != "None" ]; then |
| 282 | + echo "${platform} image already exists:" |
| 283 | + echo "$meta_img" |
| 284 | + exit 0 |
| 285 | + fi |
| 286 | + fi |
| 287 | + |
| 288 | + # reread these values from the build itself rather than rely on the ones loaded |
| 289 | + # by prepare_build since the config might've changed since then |
| 290 | + name=$(meta_key name) |
| 291 | + |
| 292 | + image_format=raw |
| 293 | + if [[ "${platform}" == "qemu" || "${platform}" == "qemu-secex" ]]; then |
| 294 | + image_format=qcow2 |
| 295 | + fi |
| 296 | + |
| 297 | + imgname=${name}-${build}-${platform}.${basearch}.${image_format} |
| 298 | + imgpath=${PWD}/${imgname} |
| 299 | + |
| 300 | + # In the jenkins pipelines we build the qemu image first and that operation |
| 301 | + # will do a lot of the same work required for later artifacts (metal, metal4k, etc) |
| 302 | + # so we want the cached output from that run to persist. The later artifacts get |
| 303 | + # built in parallel, so we need to be able to access the cache by multiple processes, |
| 304 | + # so for those we'll set `snapshot=on` so that each will get their own disk image. |
| 305 | + # This is OK because we don't checkpoint (cache) any of those stages. |
| 306 | + [ "${platform}" == "qemu" ] && snapshot="off" || snapshot="on" |
| 307 | + runvm_osbuild_config_json="$(generate_runvm_osbuild_config)" |
| 308 | + outdir=$(mktemp -p "${tmp_builddir}" -d) |
| 309 | + runvm_with_cache_snapshot "$snapshot" -- /usr/lib/coreos-assembler/runvm-osbuild \ |
| 310 | + --config "${runvm_osbuild_config_json}" \ |
| 311 | + --mpp "/usr/lib/coreos-assembler/osbuild-manifests/coreos.osbuild.${basearch}.mpp.yaml" \ |
| 312 | + --outdir "${outdir}" \ |
| 313 | + --platform "${platform}" |
| 314 | + |
| 315 | + mv "${outdir}/${platform}/${platform}" "${imgpath}" |
| 316 | + |
| 317 | + case "$platform" in |
| 318 | + qemu-secex) |
| 319 | + # Massage the generated artifact through an extra VM for secex. This |
| 320 | + # will also create an Ignition pubkey and store it in the meta.json |
| 321 | + # and builddir. |
| 322 | + postprocess_qemu_secex |
| 323 | + # Also need to update the meta.json and builddir with the main artifact. |
| 324 | + postprocess_artifact "${platform}" "${imgpath}" "${imgname}" 'False' |
| 325 | + ;; |
| 326 | + *) |
| 327 | + # Update the meta.json and builddir with the generated artifact. |
| 328 | + postprocess_artifact "${platform}" "${imgpath}" "${imgname}" 'False' |
| 329 | + ;; |
| 330 | + esac |
| 331 | + |
| 332 | + # clean up the tmpbuild |
| 333 | + rm -rf "${tmp_builddir}" |
| 334 | +} |
| 335 | + |
| 336 | +main "$@" |
0 commit comments