diff options
22 files changed, 431 insertions, 392 deletions
| @@ -1 +1,2 @@ | |||
| 1 | __pycache__ | 1 | __pycache__ |
| 2 | *.pyc | ||
diff --git a/README.adoc b/README.adoc index 47c0a2b..b4608d5 100644 --- a/README.adoc +++ b/README.adoc | |||
| @@ -122,3 +122,19 @@ garage-push --repo=/path/to/ostree-repo --ref=mybranch --credentials=/path/to/cr | |||
| 122 | .... | 122 | .... |
| 123 | 123 | ||
| 124 | You can set SOTA_PACKED_CREDENTIALS in your local.conf to make your build results be automatically synchronized with a remote server. Credentials are stored in the JSON format described in the https://github.com/advancedtelematic/aktualizr/blob/master/README.sotatools.adoc[garage-push README]. This JSON file can be optionally stored inside a zip file, although if it is stored this way, the JSON file must be named treehub.json. | 124 | You can set SOTA_PACKED_CREDENTIALS in your local.conf to make your build results be automatically synchronized with a remote server. Credentials are stored in the JSON format described in the https://github.com/advancedtelematic/aktualizr/blob/master/README.sotatools.adoc[garage-push README]. This JSON file can be optionally stored inside a zip file, although if it is stored this way, the JSON file must be named treehub.json. |
| 125 | |||
| 126 | === QA | ||
| 127 | |||
| 128 | This layer relies on the test framework oe-selftest for quality assurance. Follow the steps below to run the tests: | ||
| 129 | |||
| 130 | * Append the line below to conf/local.conf | ||
| 131 | |||
| 132 | ``` | ||
| 133 | SANITY_TESTED_DISTROS="" | ||
| 134 | ``` | ||
| 135 | |||
| 136 | * Run oe-selftest: | ||
| 137 | |||
| 138 | ``` | ||
| 139 | oe-selftest --run-tests updater | ||
| 140 | ``` | ||
diff --git a/classes/image_types_ostree.bbclass b/classes/image_types_ostree.bbclass index 1f8e195..172f2c8 100644 --- a/classes/image_types_ostree.bbclass +++ b/classes/image_types_ostree.bbclass | |||
| @@ -5,6 +5,7 @@ inherit image | |||
| 5 | IMAGE_DEPENDS_ostree = "ostree-native:do_populate_sysroot \ | 5 | IMAGE_DEPENDS_ostree = "ostree-native:do_populate_sysroot \ |
| 6 | openssl-native:do_populate_sysroot \ | 6 | openssl-native:do_populate_sysroot \ |
| 7 | coreutils-native:do_populate_sysroot \ | 7 | coreutils-native:do_populate_sysroot \ |
| 8 | unzip-native:do_populate_sysroot \ | ||
| 8 | virtual/kernel:do_deploy \ | 9 | virtual/kernel:do_deploy \ |
| 9 | ${OSTREE_INITRAMFS_IMAGE}:do_image_complete" | 10 | ${OSTREE_INITRAMFS_IMAGE}:do_image_complete" |
| 10 | 11 | ||
| @@ -104,6 +105,7 @@ IMAGE_CMD_ostree () { | |||
| 104 | if [ -d root ] && [ ! -L root ]; then | 105 | if [ -d root ] && [ ! -L root ]; then |
| 105 | if [ "$(ls -A root)" ]; then | 106 | if [ "$(ls -A root)" ]; then |
| 106 | bberror "Data in /root directory is not preserved by OSTree." | 107 | bberror "Data in /root directory is not preserved by OSTree." |
| 108 | exit 1 | ||
| 107 | fi | 109 | fi |
| 108 | 110 | ||
| 109 | if [ -n "$SYSTEMD_USED" ]; then | 111 | if [ -n "$SYSTEMD_USED" ]; then |
| @@ -159,7 +161,7 @@ IMAGE_CMD_ostree () { | |||
| 159 | } | 161 | } |
| 160 | 162 | ||
| 161 | IMAGE_TYPEDEP_ostreepush = "ostree" | 163 | IMAGE_TYPEDEP_ostreepush = "ostree" |
| 162 | IMAGE_DEPENDS_ostreepush = "aktualizr-native:do_populate_sysroot" | 164 | IMAGE_DEPENDS_ostreepush = "aktualizr-native:do_populate_sysroot ca-certificates-native:do_populate_sysroot " |
| 163 | IMAGE_CMD_ostreepush () { | 165 | IMAGE_CMD_ostreepush () { |
| 164 | # Print warnings if credetials are not set or if the file has not been found. | 166 | # Print warnings if credetials are not set or if the file has not been found. |
| 165 | if [ -n "${SOTA_PACKED_CREDENTIALS}" ]; then | 167 | if [ -n "${SOTA_PACKED_CREDENTIALS}" ]; then |
| @@ -176,4 +178,58 @@ IMAGE_CMD_ostreepush () { | |||
| 176 | fi | 178 | fi |
| 177 | } | 179 | } |
| 178 | 180 | ||
| 181 | IMAGE_TYPEDEP_garagesign = "ostreepush" | ||
| 182 | IMAGE_DEPENDS_garagesign = "garage-sign-native:do_populate_sysroot" | ||
| 183 | IMAGE_CMD_garagesign () { | ||
| 184 | if [ -n "${SOTA_PACKED_CREDENTIALS}" ]; then | ||
| 185 | # if credentials are issued by a server that doesn't support offline signing, exit silently | ||
| 186 | unzip -p ${SOTA_PACKED_CREDENTIALS} root.json targets.pub targets.sec 2>&1 >/dev/null || exit 0 | ||
| 187 | |||
| 188 | java_version=$( java -version 2>&1 | awk -F '"' '/version/ {print $2}' ) | ||
| 189 | if [ "${java_version}" = "" ]; then | ||
| 190 | bberror "Java is required for synchronization with update backend, but is not installed on the host machine" | ||
| 191 | exit 1 | ||
| 192 | elif [ "${java_version}" \< "1.8" ]; then | ||
| 193 | bberror "Java version >= 8 is required for synchronization with update backend" | ||
| 194 | exit 1 | ||
| 195 | fi | ||
| 196 | |||
| 197 | if [ ! -d "${GARAGE_SIGN_REPO}" ]; then | ||
| 198 | garage-sign init --repo ${GARAGE_SIGN_REPO} --home-dir ${GARAGE_SIGN_REPO} --credentials ${SOTA_PACKED_CREDENTIALS} | ||
| 199 | fi | ||
| 200 | |||
| 201 | if [ -n "${GARAGE_SIGN_REPOSERVER}" ]; then | ||
| 202 | reposerver_args="--reposerver ${GARAGE_SIGN_REPOSERVER}" | ||
| 203 | else | ||
| 204 | reposerver_args="" | ||
| 205 | fi | ||
| 206 | |||
| 207 | ostree_target_hash=$(cat ${OSTREE_REPO}/refs/heads/${OSTREE_BRANCHNAME}) | ||
| 208 | |||
| 209 | # Push may fail due to race condition when multiple build machines try to push simultaneously | ||
| 210 | # in which case targets.json should be pulled again and the whole procedure repeated | ||
| 211 | push_success=0 | ||
| 212 | for push_retries in $( seq 3 ); do | ||
| 213 | garage-sign targets pull --repo ${GARAGE_SIGN_REPO} --home-dir ${GARAGE_SIGN_REPO} ${reposerver_args} | ||
| 214 | garage-sign targets add --repo ${GARAGE_SIGN_REPO} --home-dir ${GARAGE_SIGN_REPO} --name ${OSTREE_BRANCHNAME} --format OSTREE --version ${OSTREE_BRANCHNAME} --length 0 --url "https://example.com/" --sha256 ${ostree_target_hash} --hardwareids ${MACHINE} | ||
| 215 | garage-sign targets sign --repo ${GARAGE_SIGN_REPO} --home-dir ${GARAGE_SIGN_REPO} --key-name=targets | ||
| 216 | errcode=0 | ||
| 217 | garage-sign targets push --repo ${GARAGE_SIGN_REPO} --home-dir ${GARAGE_SIGN_REPO} ${reposerver_args} || errcode=$? | ||
| 218 | if [ "$errcode" -eq "0" ]; then | ||
| 219 | push_success=1 | ||
| 220 | break | ||
| 221 | else | ||
| 222 | bbwarn "Push to garage repository has failed, retrying" | ||
| 223 | fi | ||
| 224 | done | ||
| 225 | |||
| 226 | if [ "$push_success" -ne "1" ]; then | ||
| 227 | bberror "Couldn't push to garage repository" | ||
| 228 | exit 1 | ||
| 229 | fi | ||
| 230 | else | ||
| 231 | bbwarn "SOTA_PACKED_CREDENTIALS not set. Please add SOTA_PACKED_CREDENTIALS." | ||
| 232 | fi | ||
| 233 | } | ||
| 234 | |||
| 179 | # vim:set ts=4 sw=4 sts=4 expandtab: | 235 | # vim:set ts=4 sw=4 sts=4 expandtab: |
diff --git a/classes/sdcard_image-rpi-ota.bbclass b/classes/sdcard_image-rpi-ota.bbclass deleted file mode 100644 index 9c859fe..0000000 --- a/classes/sdcard_image-rpi-ota.bbclass +++ /dev/null | |||
| @@ -1,190 +0,0 @@ | |||
| 1 | inherit image_types | ||
| 2 | inherit linux-raspberrypi-base | ||
| 3 | |||
| 4 | # | ||
| 5 | # Create an image that can by written onto a SD card using dd. | ||
| 6 | # | ||
| 7 | # The disk layout used is: | ||
| 8 | # | ||
| 9 | # 0 -> IMAGE_ROOTFS_ALIGNMENT - reserved for other data | ||
| 10 | # IMAGE_ROOTFS_ALIGNMENT -> BOOT_SPACE - bootloader and kernel | ||
| 11 | # BOOT_SPACE -> SDIMG_OTA_SIZE - rootfs | ||
| 12 | # | ||
| 13 | |||
| 14 | # Default Free space = 1.3x | ||
| 15 | # Use IMAGE_OVERHEAD_FACTOR to add more space | ||
| 16 | # <---------> | ||
| 17 | # 4MiB 40MiB SDIMG_OTA_ROOTFS | ||
| 18 | # <-----------------------> <----------> <----------------------> | ||
| 19 | # ------------------------ ------------ ------------------------ | ||
| 20 | # | IMAGE_ROOTFS_ALIGNMENT | BOOT_SPACE | OTAROOT_SIZE | | ||
| 21 | # ------------------------ ------------ ------------------------ | ||
| 22 | # ^ ^ ^ ^ | ||
| 23 | # | | | | | ||
| 24 | # 0 4MiB 4MiB + 40MiB 4MiB + 40Mib + SDIMG_OTA_ROOTFS | ||
| 25 | |||
| 26 | # This image depends on the rootfs image | ||
| 27 | IMAGE_TYPEDEP_rpi-sdimg-ota = "${SDIMG_OTA_ROOTFS_TYPE}" | ||
| 28 | |||
| 29 | # Set kernel and boot loader | ||
| 30 | IMAGE_BOOTLOADER ?= "bcm2835-bootfiles" | ||
| 31 | |||
| 32 | # Set initramfs extension | ||
| 33 | KERNEL_INITRAMFS ?= "" | ||
| 34 | |||
| 35 | # Kernel image name | ||
| 36 | SDIMG_OTA_KERNELIMAGE_raspberrypi ?= "kernel.img" | ||
| 37 | SDIMG_OTA_KERNELIMAGE_raspberrypi2 ?= "kernel7.img" | ||
| 38 | SDIMG_OTA_KERNELIMAGE_raspberrypi3 ?= "kernel7.img" | ||
| 39 | |||
| 40 | # Boot partition volume id | ||
| 41 | BOOTDD_VOLUME_ID ?= "${MACHINE}" | ||
| 42 | |||
| 43 | # Boot partition size [in KiB] (will be rounded up to IMAGE_ROOTFS_ALIGNMENT) | ||
| 44 | BOOT_SPACE ?= "40960" | ||
| 45 | |||
| 46 | # Set alignment to 4MB [in KiB] | ||
| 47 | IMAGE_ROOTFS_ALIGNMENT = "4096" | ||
| 48 | |||
| 49 | # Use an uncompressed ext3 by default as rootfs | ||
| 50 | SDIMG_OTA_ROOTFS_TYPE ?= "otaimg" | ||
| 51 | SDIMG_OTA_ROOTFS = "${DEPLOY_DIR_IMAGE}/${IMAGE_LINK_NAME}.${SDIMG_OTA_ROOTFS_TYPE}" | ||
| 52 | |||
| 53 | IMAGE_DEPENDS_rpi-sdimg-ota = " \ | ||
| 54 | parted-native \ | ||
| 55 | mtools-native \ | ||
| 56 | dosfstools-native \ | ||
| 57 | virtual/kernel:do_deploy \ | ||
| 58 | ${IMAGE_BOOTLOADER} \ | ||
| 59 | u-boot \ | ||
| 60 | " | ||
| 61 | IMAGE_TYPEDEP_rpi-sdimg-ota = "otaimg" | ||
| 62 | |||
| 63 | # SD card image name | ||
| 64 | SDIMG_OTA = "${IMGDEPLOYDIR}/${IMAGE_NAME}.rootfs.rpi-sdimg-ota" | ||
| 65 | |||
| 66 | # Compression method to apply to SDIMG_OTA after it has been created. Supported | ||
| 67 | # compression formats are "gzip", "bzip2" or "xz". The original .rpi-sdimg-ota file | ||
| 68 | # is kept and a new compressed file is created if one of these compression | ||
| 69 | # formats is chosen. If SDIMG_OTA_COMPRESSION is set to any other value it is | ||
| 70 | # silently ignored. | ||
| 71 | #SDIMG_OTA_COMPRESSION ?= "" | ||
| 72 | |||
| 73 | # Additional files and/or directories to be copied into the vfat partition from the IMAGE_ROOTFS. | ||
| 74 | FATPAYLOAD ?= "" | ||
| 75 | |||
| 76 | IMAGE_CMD_rpi-sdimg-ota () { | ||
| 77 | |||
| 78 | # Align partitions | ||
| 79 | OTAROOT_SIZE=`du -Lb ${SDIMG_OTA_ROOTFS} | cut -f1` | ||
| 80 | OTAROOT_SIZE=$(expr ${OTAROOT_SIZE} / 1024 + 1) | ||
| 81 | BOOT_SPACE_ALIGNED=$(expr ${BOOT_SPACE} + ${IMAGE_ROOTFS_ALIGNMENT} - 1) | ||
| 82 | BOOT_SPACE_ALIGNED=$(expr ${BOOT_SPACE_ALIGNED} - ${BOOT_SPACE_ALIGNED} % ${IMAGE_ROOTFS_ALIGNMENT}) | ||
| 83 | SDIMG_OTA_SIZE=$(expr ${IMAGE_ROOTFS_ALIGNMENT} + ${BOOT_SPACE_ALIGNED} + $OTAROOT_SIZE) | ||
| 84 | |||
| 85 | echo "Creating filesystem with Boot partition ${BOOT_SPACE_ALIGNED} KiB and RootFS $OTAROOT_SIZE KiB" | ||
| 86 | |||
| 87 | # Check if we are building with device tree support | ||
| 88 | DTS="${@get_dts(d, None)}" | ||
| 89 | |||
| 90 | # Initialize sdcard image file | ||
| 91 | dd if=/dev/zero of=${SDIMG_OTA} bs=1024 count=0 seek=${SDIMG_OTA_SIZE} | ||
| 92 | |||
| 93 | # Create partition table | ||
| 94 | parted -s ${SDIMG_OTA} mklabel msdos | ||
| 95 | # Create boot partition and mark it as bootable | ||
| 96 | parted -s ${SDIMG_OTA} unit KiB mkpart primary fat32 ${IMAGE_ROOTFS_ALIGNMENT} $(expr ${BOOT_SPACE_ALIGNED} \+ ${IMAGE_ROOTFS_ALIGNMENT}) | ||
| 97 | parted -s ${SDIMG_OTA} set 1 boot on | ||
| 98 | # Create rootfs partition to the end of disk | ||
| 99 | parted -s ${SDIMG_OTA} -- unit KiB mkpart primary ext2 $(expr ${BOOT_SPACE_ALIGNED} \+ ${IMAGE_ROOTFS_ALIGNMENT}) -1s | ||
| 100 | parted ${SDIMG_OTA} print | ||
| 101 | |||
| 102 | # Create a vfat image with boot files | ||
| 103 | BOOT_BLOCKS=$(LC_ALL=C parted -s ${SDIMG_OTA} unit b print | awk '/ 1 / { print substr($4, 1, length($4 -1)) / 512 /2 }') | ||
| 104 | rm -f ${WORKDIR}/boot.img | ||
| 105 | mkfs.vfat -n "${BOOTDD_VOLUME_ID}" -S 512 -C ${WORKDIR}/boot.img $BOOT_BLOCKS | ||
| 106 | sync | ||
| 107 | |||
| 108 | mcopy -i ${WORKDIR}/boot.img -s ${DEPLOY_DIR_IMAGE}/bcm2835-bootfiles/* ::/ | ||
| 109 | |||
| 110 | if test -n "${DTS}"; then | ||
| 111 | # Device Tree Overlays are assumed to be suffixed by '-overlay.dtb' string and will be put in a dedicated folder | ||
| 112 | DT_OVERLAYS="${@split_overlays(d, 0)}" | ||
| 113 | DT_ROOT="${@split_overlays(d, 1)}" | ||
| 114 | |||
| 115 | # Copy board device trees to root folder | ||
| 116 | for DTB in ${DT_ROOT}; do | ||
| 117 | DTB_BASE_NAME=`basename ${DTB} .dtb` | ||
| 118 | |||
| 119 | mcopy -i ${WORKDIR}/boot.img -s ${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}-${DTB_BASE_NAME}.dtb ::${DTB_BASE_NAME}.dtb | ||
| 120 | done | ||
| 121 | |||
| 122 | # Copy device tree overlays to dedicated folder | ||
| 123 | mmd -i ${WORKDIR}/boot.img overlays | ||
| 124 | for DTB in ${DT_OVERLAYS}; do | ||
| 125 | DTB_EXT=${DTB##*.} | ||
| 126 | DTB_BASE_NAME=`basename ${DTB} ."${DTB_EXT}"` | ||
| 127 | |||
| 128 | mcopy -i ${WORKDIR}/boot.img -s ${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE}-${DTB_BASE_NAME}.${DTB_EXT} ::overlays/${DTB_BASE_NAME}.${DTB_EXT} | ||
| 129 | done | ||
| 130 | fi | ||
| 131 | |||
| 132 | case "${KERNEL_IMAGETYPE}" in | ||
| 133 | "uImage") | ||
| 134 | mcopy -i ${WORKDIR}/boot.img -s ${DEPLOY_DIR_IMAGE}/u-boot.bin ::${SDIMG_OTA_KERNELIMAGE} | ||
| 135 | ;; | ||
| 136 | *) | ||
| 137 | bbfatal "Kernel uImage is required for OTA image. Please set KERNEL_IMAGETYPE to \"uImage\"" | ||
| 138 | ;; | ||
| 139 | esac | ||
| 140 | |||
| 141 | if [ -n ${FATPAYLOAD} ] ; then | ||
| 142 | echo "Copying payload into VFAT" | ||
| 143 | for entry in ${FATPAYLOAD} ; do | ||
| 144 | # add the || true to stop aborting on vfat issues like not supporting .~lock files | ||
| 145 | mcopy -i ${WORKDIR}/boot.img -s -v ${IMAGE_ROOTFS}$entry :: || true | ||
| 146 | done | ||
| 147 | fi | ||
| 148 | |||
| 149 | # Add stamp file | ||
| 150 | echo "${IMAGE_NAME}" > ${WORKDIR}/image-version-info | ||
| 151 | mcopy -i ${WORKDIR}/boot.img -v ${WORKDIR}//image-version-info :: | ||
| 152 | |||
| 153 | # Burn Partitions | ||
| 154 | sync | ||
| 155 | dd if=${WORKDIR}/boot.img of=${SDIMG_OTA} conv=notrunc seek=1 bs=$(expr ${IMAGE_ROOTFS_ALIGNMENT} \* 1024) && sync && sync | ||
| 156 | # If SDIMG_OTA_ROOTFS_TYPE is a .xz file use xzcat | ||
| 157 | if echo "${SDIMG_OTA_ROOTFS_TYPE}" | egrep -q "*\.xz" | ||
| 158 | then | ||
| 159 | xzcat ${SDIMG_OTA_ROOTFS} | dd of=${SDIMG_OTA} conv=notrunc seek=1 bs=$(expr 1024 \* ${BOOT_SPACE_ALIGNED} + ${IMAGE_ROOTFS_ALIGNMENT} \* 1024) && sync && sync | ||
| 160 | else | ||
| 161 | dd if=${SDIMG_OTA_ROOTFS} of=${SDIMG_OTA} conv=notrunc seek=1 bs=$(expr 1024 \* ${BOOT_SPACE_ALIGNED} + ${IMAGE_ROOTFS_ALIGNMENT} \* 1024) && sync && sync | ||
| 162 | fi | ||
| 163 | |||
| 164 | # Optionally apply compression | ||
| 165 | case "${SDIMG_OTA_COMPRESSION}" in | ||
| 166 | "gzip") | ||
| 167 | gzip -k9 "${SDIMG_OTA}" | ||
| 168 | ;; | ||
| 169 | "bzip2") | ||
| 170 | bzip2 -k9 "${SDIMG_OTA}" | ||
| 171 | ;; | ||
| 172 | "xz") | ||
| 173 | xz -k "${SDIMG_OTA}" | ||
| 174 | ;; | ||
| 175 | esac | ||
| 176 | } | ||
| 177 | |||
| 178 | ROOTFS_POSTPROCESS_COMMAND += " rpi_generate_sysctl_config ; " | ||
| 179 | |||
| 180 | rpi_generate_sysctl_config() { | ||
| 181 | # systemd sysctl config | ||
| 182 | test -d ${IMAGE_ROOTFS}${sysconfdir}/sysctl.d && \ | ||
| 183 | echo "vm.min_free_kbytes = 8192" > ${IMAGE_ROOTFS}${sysconfdir}/sysctl.d/rpi-vm.conf | ||
| 184 | |||
| 185 | # sysv sysctl config | ||
| 186 | IMAGE_SYSCTL_CONF="${IMAGE_ROOTFS}${sysconfdir}/sysctl.conf" | ||
| 187 | test -e ${IMAGE_ROOTFS}${sysconfdir}/sysctl.conf && \ | ||
| 188 | sed -e "/vm.min_free_kbytes/d" -i ${IMAGE_SYSCTL_CONF} | ||
| 189 | echo "" >> ${IMAGE_SYSCTL_CONF} && echo "vm.min_free_kbytes = 8192" >> ${IMAGE_SYSCTL_CONF} | ||
| 190 | } | ||
diff --git a/classes/sota.bbclass b/classes/sota.bbclass index 1865356..f5a42c1 100644 --- a/classes/sota.bbclass +++ b/classes/sota.bbclass | |||
| @@ -5,11 +5,13 @@ python __anonymous() { | |||
| 5 | 5 | ||
| 6 | OVERRIDES .= "${@bb.utils.contains('DISTRO_FEATURES', 'sota', ':sota', '', d)}" | 6 | OVERRIDES .= "${@bb.utils.contains('DISTRO_FEATURES', 'sota', ':sota', '', d)}" |
| 7 | 7 | ||
| 8 | HOSTTOOLS_NONFATAL += "java" | ||
| 9 | |||
| 8 | SOTA_CLIENT ??= "aktualizr" | 10 | SOTA_CLIENT ??= "aktualizr" |
| 9 | SOTA_CLIENT_PROV ??= "aktualizr-auto-prov" | 11 | SOTA_CLIENT_PROV ??= "aktualizr-auto-prov" |
| 10 | IMAGE_INSTALL_append_sota = " ostree os-release ${SOTA_CLIENT} ${SOTA_CLIENT_PROV}" | 12 | IMAGE_INSTALL_append_sota = " ostree os-release ${SOTA_CLIENT} ${SOTA_CLIENT_PROV}" |
| 11 | IMAGE_CLASSES += " image_types_ostree image_types_ota" | 13 | IMAGE_CLASSES += " image_types_ostree image_types_ota" |
| 12 | IMAGE_FSTYPES += "${@bb.utils.contains('DISTRO_FEATURES', 'sota', 'ostreepush otaimg wic', ' ', d)}" | 14 | IMAGE_FSTYPES += "${@bb.utils.contains('DISTRO_FEATURES', 'sota', 'ostreepush garagesign otaimg wic', ' ', d)}" |
| 13 | 15 | ||
| 14 | PACKAGECONFIG_append_pn-curl = "${@bb.utils.contains('SOTA_CLIENT_FEATURES', 'hsm', " ssl", " ", d)}" | 16 | PACKAGECONFIG_append_pn-curl = "${@bb.utils.contains('SOTA_CLIENT_FEATURES', 'hsm', " ssl", " ", d)}" |
| 15 | PACKAGECONFIG_remove_pn-curl = "${@bb.utils.contains('SOTA_CLIENT_FEATURES', 'hsm', " gnutls", " ", d)}" | 17 | PACKAGECONFIG_remove_pn-curl = "${@bb.utils.contains('SOTA_CLIENT_FEATURES', 'hsm', " gnutls", " ", d)}" |
| @@ -25,6 +27,11 @@ OSTREE_BRANCHNAME ?= "${MACHINE}" | |||
| 25 | OSTREE_OSNAME ?= "poky" | 27 | OSTREE_OSNAME ?= "poky" |
| 26 | OSTREE_INITRAMFS_IMAGE ?= "initramfs-ostree-image" | 28 | OSTREE_INITRAMFS_IMAGE ?= "initramfs-ostree-image" |
| 27 | 29 | ||
| 30 | |||
| 31 | GARAGE_SIGN_REPO ?= "${DEPLOY_DIR_IMAGE}/garage_sign_repo" | ||
| 32 | GARAGE_SIGN_KEYNAME ?= "garage-key" | ||
| 33 | GARAGE_TARGET_NAME ?= "${OSTREE_BRANCHNAME}" | ||
| 34 | |||
| 28 | SOTA_MACHINE ??="none" | 35 | SOTA_MACHINE ??="none" |
| 29 | SOTA_MACHINE_raspberrypi2 ?= "raspberrypi" | 36 | SOTA_MACHINE_raspberrypi2 ?= "raspberrypi" |
| 30 | SOTA_MACHINE_raspberrypi3 ?= "raspberrypi" | 37 | SOTA_MACHINE_raspberrypi3 ?= "raspberrypi" |
diff --git a/classes/sota_am335x-evm-wifi.bbclass b/classes/sota_am335x-evm-wifi.bbclass index 821e8fb..adefb47 100644 --- a/classes/sota_am335x-evm-wifi.bbclass +++ b/classes/sota_am335x-evm-wifi.bbclass | |||
| @@ -1,5 +1,3 @@ | |||
| 1 | IMAGE_CLASSES += "image_types_uboot" | ||
| 2 | |||
| 3 | KERNEL_IMAGETYPE_sota = "uImage" | 1 | KERNEL_IMAGETYPE_sota = "uImage" |
| 4 | 2 | ||
| 5 | OSTREE_BOOTLOADER ?= "u-boot" | 3 | OSTREE_BOOTLOADER ?= "u-boot" |
diff --git a/classes/sota_m3ulcb.bbclass b/classes/sota_m3ulcb.bbclass index 21d04ba..6b63af4 100644 --- a/classes/sota_m3ulcb.bbclass +++ b/classes/sota_m3ulcb.bbclass | |||
| @@ -2,7 +2,6 @@ | |||
| 2 | OSTREE_KERNEL = "Image" | 2 | OSTREE_KERNEL = "Image" |
| 3 | 3 | ||
| 4 | EXTRA_IMAGEDEPENDS_append_sota = " m3ulcb-ota-bootfiles" | 4 | EXTRA_IMAGEDEPENDS_append_sota = " m3ulcb-ota-bootfiles" |
| 5 | IMAGE_CLASSES_append_sota = " image_types_uboot " | ||
| 6 | IMAGE_BOOT_FILES_sota += "m3ulcb-ota-bootfiles/*" | 5 | IMAGE_BOOT_FILES_sota += "m3ulcb-ota-bootfiles/*" |
| 7 | 6 | ||
| 8 | OSTREE_BOOTLOADER ?= "u-boot" | 7 | OSTREE_BOOTLOADER ?= "u-boot" |
diff --git a/classes/sota_porter.bbclass b/classes/sota_porter.bbclass index a8f5ba1..75ae579 100644 --- a/classes/sota_porter.bbclass +++ b/classes/sota_porter.bbclass | |||
| @@ -2,7 +2,6 @@ | |||
| 2 | OSTREE_KERNEL = "uImage+dtb" | 2 | OSTREE_KERNEL = "uImage+dtb" |
| 3 | 3 | ||
| 4 | EXTRA_IMAGEDEPENDS_append_sota = " porter-bootfiles" | 4 | EXTRA_IMAGEDEPENDS_append_sota = " porter-bootfiles" |
| 5 | IMAGE_CLASSES_append_sota = " image_types_uboot " | ||
| 6 | IMAGE_BOOT_FILES_sota += "porter-bootfiles/*" | 5 | IMAGE_BOOT_FILES_sota += "porter-bootfiles/*" |
| 7 | 6 | ||
| 8 | OSTREE_BOOTLOADER ?= "u-boot" | 7 | OSTREE_BOOTLOADER ?= "u-boot" |
diff --git a/classes/sota_raspberrypi.bbclass b/classes/sota_raspberrypi.bbclass index cc6b666..51d07b2 100644 --- a/classes/sota_raspberrypi.bbclass +++ b/classes/sota_raspberrypi.bbclass | |||
| @@ -1,11 +1,9 @@ | |||
| 1 | IMAGE_CLASSES += "${@bb.utils.contains('DISTRO_FEATURES', 'sota', 'image_types_uboot sdcard_image-rpi-ota', '', d)}" | ||
| 2 | IMAGE_FSTYPES += "${@bb.utils.contains('DISTRO_FEATURES', 'sota', 'rpi-sdimg-ota.xz', 'rpi-sdimg.xz', d)}" | ||
| 3 | |||
| 4 | IMAGE_FSTYPES_remove = "${@bb.utils.contains('DISTRO_FEATURES', 'sota', 'wic rpi-sdimg rpi-sdimg.xz', '', d)}" | ||
| 5 | |||
| 6 | KERNEL_IMAGETYPE_sota = "uImage" | 1 | KERNEL_IMAGETYPE_sota = "uImage" |
| 7 | PREFERRED_PROVIDER_virtual/bootloader_sota ?= "u-boot" | 2 | PREFERRED_PROVIDER_virtual/bootloader_sota ?= "u-boot" |
| 8 | UBOOT_MACHINE_raspberrypi2_sota ?= "rpi_2_defconfig" | 3 | UBOOT_MACHINE_raspberrypi2_sota ?= "rpi_2_defconfig" |
| 9 | UBOOT_MACHINE_raspberrypi3_sota ?= "rpi_3_32b_defconfig" | 4 | UBOOT_MACHINE_raspberrypi3_sota ?= "rpi_3_32b_defconfig" |
| 10 | 5 | ||
| 11 | OSTREE_BOOTLOADER ?= "u-boot" | 6 | OSTREE_BOOTLOADER ?= "u-boot" |
| 7 | |||
| 8 | # OSTree puts its own boot.scr to bcm2835-bootfiles | ||
| 9 | IMAGE_BOOT_FILES_remove_sota += "boot.scr" | ||
diff --git a/lib/oeqa/selftest/garage_push.py b/lib/oeqa/selftest/garage_push.py deleted file mode 100644 index 3490de5..0000000 --- a/lib/oeqa/selftest/garage_push.py +++ /dev/null | |||
| @@ -1,39 +0,0 @@ | |||
| 1 | import unittest | ||
| 2 | import os | ||
| 3 | import logging | ||
| 4 | |||
| 5 | from oeqa.selftest.base import oeSelfTest | ||
| 6 | from oeqa.utils.commands import runCmd, bitbake, get_bb_var | ||
| 7 | |||
| 8 | class GaragePushTests(oeSelfTest): | ||
| 9 | |||
| 10 | @classmethod | ||
| 11 | def setUpClass(cls): | ||
| 12 | # Ensure we have the right data in pkgdata | ||
| 13 | logger = logging.getLogger("selftest") | ||
| 14 | logger.info('Running bitbake to build aktualizr-native tools') | ||
| 15 | bitbake('aktualizr-native garage-sign-native') | ||
| 16 | |||
| 17 | def test_help(self): | ||
| 18 | image_dir = get_bb_var("D", "aktualizr-native") | ||
| 19 | bin_dir = get_bb_var("bindir", "aktualizr-native") | ||
| 20 | gp_path = os.path.join(image_dir, bin_dir[1:], 'garage-push') | ||
| 21 | result = runCmd('%s --help' % gp_path, ignore_status=True) | ||
| 22 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 23 | |||
| 24 | def test_java(self): | ||
| 25 | result = runCmd('which java', ignore_status=True) | ||
| 26 | self.assertEqual(result.status, 0, "Java not found.") | ||
| 27 | |||
| 28 | def test_sign(self): | ||
| 29 | image_dir = get_bb_var("D", "garage-sign-native") | ||
| 30 | bin_dir = get_bb_var("bindir", "garage-sign-native") | ||
| 31 | gs_path = os.path.join(image_dir, bin_dir[1:], 'garage-sign') | ||
| 32 | result = runCmd('%s --help' % gs_path, ignore_status=True) | ||
| 33 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 34 | |||
| 35 | def test_push(self): | ||
| 36 | bitbake('core-image-minimal') | ||
| 37 | self.write_config('IMAGE_INSTALL_append = " man "') | ||
| 38 | bitbake('core-image-minimal') | ||
| 39 | |||
diff --git a/lib/oeqa/selftest/qemucommand.py b/lib/oeqa/selftest/qemucommand.py new file mode 120000 index 0000000..bc06dde --- /dev/null +++ b/lib/oeqa/selftest/qemucommand.py | |||
| @@ -0,0 +1 @@ | |||
| ../../../scripts/qemucommand.py \ No newline at end of file | |||
diff --git a/lib/oeqa/selftest/updater.py b/lib/oeqa/selftest/updater.py new file mode 100644 index 0000000..2723b4a --- /dev/null +++ b/lib/oeqa/selftest/updater.py | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | import unittest | ||
| 2 | import os | ||
| 3 | import logging | ||
| 4 | import subprocess | ||
| 5 | import time | ||
| 6 | |||
| 7 | from oeqa.selftest.base import oeSelfTest | ||
| 8 | from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars | ||
| 9 | from oeqa.selftest.qemucommand import QemuCommand | ||
| 10 | |||
| 11 | |||
| 12 | class SotaToolsTests(oeSelfTest): | ||
| 13 | |||
| 14 | @classmethod | ||
| 15 | def setUpClass(cls): | ||
| 16 | logger = logging.getLogger("selftest") | ||
| 17 | logger.info('Running bitbake to build aktualizr-native tools') | ||
| 18 | bitbake('aktualizr-native') | ||
| 19 | |||
| 20 | def test_push_help(self): | ||
| 21 | bb_vars = get_bb_vars(['SYSROOT_DESTDIR', 'bindir'], 'aktualizr-native') | ||
| 22 | p = bb_vars['SYSROOT_DESTDIR'] + bb_vars['bindir'] + "/" + "garage-push" | ||
| 23 | self.assertTrue(os.path.isfile(p), msg = "No garage-push found (%s)" % p) | ||
| 24 | result = runCmd('%s --help' % p, ignore_status=True) | ||
| 25 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 26 | |||
| 27 | def test_deploy_help(self): | ||
| 28 | bb_vars = get_bb_vars(['SYSROOT_DESTDIR', 'bindir'], 'aktualizr-native') | ||
| 29 | p = bb_vars['SYSROOT_DESTDIR'] + bb_vars['bindir'] + "/" + "garage-deploy" | ||
| 30 | self.assertTrue(os.path.isfile(p), msg = "No garage-deploy found (%s)" % p) | ||
| 31 | result = runCmd('%s --help' % p, ignore_status=True) | ||
| 32 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 33 | |||
| 34 | |||
| 35 | class GarageSignTests(oeSelfTest): | ||
| 36 | |||
| 37 | @classmethod | ||
| 38 | def setUpClass(cls): | ||
| 39 | logger = logging.getLogger("selftest") | ||
| 40 | logger.info('Running bitbake to build garage-sign-native') | ||
| 41 | bitbake('garage-sign-native') | ||
| 42 | |||
| 43 | def test_help(self): | ||
| 44 | bb_vars = get_bb_vars(['SYSROOT_DESTDIR', 'bindir'], 'garage-sign-native') | ||
| 45 | p = bb_vars['SYSROOT_DESTDIR'] + bb_vars['bindir'] + "/" + "garage-sign" | ||
| 46 | self.assertTrue(os.path.isfile(p), msg = "No garage-sign found (%s)" % p) | ||
| 47 | result = runCmd('%s --help' % p, ignore_status=True) | ||
| 48 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 49 | |||
| 50 | |||
| 51 | class HsmTests(oeSelfTest): | ||
| 52 | |||
| 53 | def test_hsm(self): | ||
| 54 | self.write_config('SOTA_CLIENT_FEATURES="hsm hsm-test"') | ||
| 55 | bitbake('core-image-minimal') | ||
| 56 | |||
| 57 | |||
| 58 | class GeneralTests(oeSelfTest): | ||
| 59 | |||
| 60 | def test_feature_sota(self): | ||
| 61 | result = get_bb_var('DISTRO_FEATURES').find('sota') | ||
| 62 | self.assertNotEqual(result, -1, 'Feature "sota" not set at DISTRO_FEATURES'); | ||
| 63 | |||
| 64 | def test_feature_systemd(self): | ||
| 65 | result = get_bb_var('DISTRO_FEATURES').find('systemd') | ||
| 66 | self.assertNotEqual(result, -1, 'Feature "systemd" not set at DISTRO_FEATURES'); | ||
| 67 | |||
| 68 | def test_credentials(self): | ||
| 69 | bitbake('core-image-minimal') | ||
| 70 | credentials = get_bb_var('SOTA_PACKED_CREDENTIALS') | ||
| 71 | # skip the test if the variable SOTA_PACKED_CREDENTIALS is not set | ||
| 72 | if credentials is None: | ||
| 73 | raise unittest.SkipTest("Variable 'SOTA_PACKED_CREDENTIALS' not set.") | ||
| 74 | # Check if the file exists | ||
| 75 | self.assertTrue(os.path.isfile(credentials), "File %s does not exist" % credentials) | ||
| 76 | deploydir = get_bb_var('DEPLOY_DIR_IMAGE') | ||
| 77 | imagename = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal') | ||
| 78 | # Check if the credentials are included in the output image | ||
| 79 | result = runCmd('tar -jtvf %s/%s.tar.bz2 | grep sota_provisioning_credentials.zip' % (deploydir, imagename), ignore_status=True) | ||
| 80 | self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) | ||
| 81 | |||
| 82 | def test_java(self): | ||
| 83 | result = runCmd('which java', ignore_status=True) | ||
| 84 | self.assertEqual(result.status, 0, "Java not found.") | ||
| 85 | |||
| 86 | def test_add_package(self): | ||
| 87 | print('') | ||
| 88 | deploydir = get_bb_var('DEPLOY_DIR_IMAGE') | ||
| 89 | imagename = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal') | ||
| 90 | image_path = deploydir + '/' + imagename + '.otaimg' | ||
| 91 | logger = logging.getLogger("selftest") | ||
| 92 | |||
| 93 | logger.info('Running bitbake with man in the image package list') | ||
| 94 | self.write_config('IMAGE_INSTALL_append = " man "') | ||
| 95 | bitbake('-c cleanall man') | ||
| 96 | bitbake('core-image-minimal') | ||
| 97 | result = runCmd('oe-pkgdata-util find-path /usr/bin/man') | ||
| 98 | self.assertEqual(result.output, 'man: /usr/bin/man') | ||
| 99 | path1 = os.path.realpath(image_path) | ||
| 100 | size1 = os.path.getsize(path1) | ||
| 101 | logger.info('First image %s has size %i' % (path1, size1)) | ||
| 102 | |||
| 103 | logger.info('Running bitbake without man in the image package list') | ||
| 104 | self.write_config('IMAGE_INSTALL_remove = " man "') | ||
| 105 | bitbake('-c cleanall man') | ||
| 106 | bitbake('core-image-minimal') | ||
| 107 | result = runCmd('oe-pkgdata-util find-path /usr/bin/man', ignore_status=True) | ||
| 108 | self.assertEqual(result.status, 1, "Status different than 1. output: %s" % result.output) | ||
| 109 | self.assertEqual(result.output, 'ERROR: Unable to find any package producing path /usr/bin/man') | ||
| 110 | path2 = os.path.realpath(image_path) | ||
| 111 | size2 = os.path.getsize(path2) | ||
| 112 | logger.info('Second image %s has size %i' % (path2, size2)) | ||
| 113 | self.assertNotEqual(path1, path2, "Image paths are identical; image was not rebuilt.") | ||
| 114 | self.assertNotEqual(size1, size2, "Image sizes are identical; image was not rebuilt.") | ||
| 115 | |||
| 116 | def test_qemu(self): | ||
| 117 | print('') | ||
| 118 | # Create empty object. | ||
| 119 | args = type('', (), {})() | ||
| 120 | args.imagename = 'core-image-minimal' | ||
| 121 | args.mac = None | ||
| 122 | # Could use DEPLOY_DIR_IMAGE here but it's already in the machine | ||
| 123 | # subdirectory. | ||
| 124 | args.dir = 'tmp/deploy/images' | ||
| 125 | args.efi = False | ||
| 126 | args.machine = None | ||
| 127 | args.kvm = None # Autodetect | ||
| 128 | args.no_gui = True | ||
| 129 | args.gdb = False | ||
| 130 | args.pcap = None | ||
| 131 | args.overlay = None | ||
| 132 | args.dry_run = False | ||
| 133 | |||
| 134 | qemu_command = QemuCommand(args) | ||
| 135 | cmdline = qemu_command.command_line() | ||
| 136 | print('Booting image with run-qemu-ota...') | ||
| 137 | s = subprocess.Popen(cmdline) | ||
| 138 | time.sleep(10) | ||
| 139 | print('Machine name (hostname) of device is:') | ||
| 140 | ssh_cmd = ['ssh', '-q', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', 'root@localhost', '-p', str(qemu_command.ssh_port), 'hostname'] | ||
| 141 | s2 = subprocess.Popen(ssh_cmd) | ||
| 142 | time.sleep(5) | ||
| 143 | try: | ||
| 144 | s.terminate() | ||
| 145 | except KeyboardInterrupt: | ||
| 146 | pass | ||
| 147 | |||
diff --git a/recipes-core/images/initramfs-ostree-image.bb b/recipes-core/images/initramfs-ostree-image.bb index 4870579..4ab9da8 100644 --- a/recipes-core/images/initramfs-ostree-image.bb +++ b/recipes-core/images/initramfs-ostree-image.bb | |||
| @@ -15,7 +15,6 @@ LICENSE = "MIT" | |||
| 15 | 15 | ||
| 16 | IMAGE_FSTYPES = "ext4.gz" | 16 | IMAGE_FSTYPES = "ext4.gz" |
| 17 | IMAGE_FSTYPES_append_arm = " ext4.gz.u-boot" | 17 | IMAGE_FSTYPES_append_arm = " ext4.gz.u-boot" |
| 18 | IMAGE_CLASSES_append_arm = " image_types_uboot" | ||
| 19 | 18 | ||
| 20 | inherit core-image | 19 | inherit core-image |
| 21 | 20 | ||
diff --git a/recipes-sota/aktualizr/aktualizr-hsm-test-prov.bb b/recipes-sota/aktualizr/aktualizr-hsm-test-prov.bb index 276c17e..c443c56 100644 --- a/recipes-sota/aktualizr/aktualizr-hsm-test-prov.bb +++ b/recipes-sota/aktualizr/aktualizr-hsm-test-prov.bb | |||
| @@ -23,12 +23,12 @@ inherit systemd | |||
| 23 | do_install() { | 23 | do_install() { |
| 24 | install -d ${D}/${systemd_unitdir}/system | 24 | install -d ${D}/${systemd_unitdir}/system |
| 25 | install -m 0644 ${WORKDIR}/aktualizr-autoprovision.service ${D}/${systemd_unitdir}/system/aktualizr.service | 25 | install -m 0644 ${WORKDIR}/aktualizr-autoprovision.service ${D}/${systemd_unitdir}/system/aktualizr.service |
| 26 | install -d ${D}/usr/lib/sota | 26 | install -d ${D}${libdir}/sota |
| 27 | aktualizr_implicit_writer -c ${SOTA_PACKED_CREDENTIALS} --no-root-ca \ | 27 | aktualizr_implicit_writer -c ${SOTA_PACKED_CREDENTIALS} --no-root-ca \ |
| 28 | -i ${WORKDIR}/sota_hsm_test.toml -o ${D}/usr/lib/sota/sota.toml -p ${D} | 28 | -i ${WORKDIR}/sota_hsm_test.toml -o ${D}${libdir}/sota/sota.toml -p ${D} |
| 29 | } | 29 | } |
| 30 | 30 | ||
| 31 | FILES_${PN} = " \ | 31 | FILES_${PN} = " \ |
| 32 | ${systemd_unitdir}/system/aktualizr.service \ | 32 | ${systemd_unitdir}/system/aktualizr.service \ |
| 33 | /usr/lib/sota/sota.toml \ | 33 | ${libdir}/sota/sota.toml \ |
| 34 | " | 34 | " |
diff --git a/recipes-sota/aktualizr/aktualizr_git.bb b/recipes-sota/aktualizr/aktualizr_git.bb index 9a1a7a6..162065e 100644 --- a/recipes-sota/aktualizr/aktualizr_git.bb +++ b/recipes-sota/aktualizr/aktualizr_git.bb | |||
| @@ -18,7 +18,7 @@ PR = "7" | |||
| 18 | SRC_URI = " \ | 18 | SRC_URI = " \ |
| 19 | git://github.com/advancedtelematic/aktualizr;branch=${BRANCH} \ | 19 | git://github.com/advancedtelematic/aktualizr;branch=${BRANCH} \ |
| 20 | " | 20 | " |
| 21 | SRCREV = "67c4f44c4136d16871726449502e3926098e8524" | 21 | SRCREV = "f043191ae622a96cf2f4d48f9073d5cfa9f16e3f" |
| 22 | BRANCH ?= "master" | 22 | BRANCH ?= "master" |
| 23 | 23 | ||
| 24 | S = "${WORKDIR}/git" | 24 | S = "${WORKDIR}/git" |
| @@ -33,7 +33,6 @@ EXTRA_OECMAKE_append_class-native = "-DBUILD_SOTA_TOOLS=ON -DBUILD_OSTREE=OFF " | |||
| 33 | 33 | ||
| 34 | do_install_append () { | 34 | do_install_append () { |
| 35 | rm -f ${D}${bindir}/aktualizr_cert_provider | 35 | rm -f ${D}${bindir}/aktualizr_cert_provider |
| 36 | rm -f ${D}${bindir}/garage-deploy | ||
| 37 | } | 36 | } |
| 38 | do_install_append_class-target () { | 37 | do_install_append_class-target () { |
| 39 | rm -f ${D}${bindir}/aktualizr_implicit_writer | 38 | rm -f ${D}${bindir}/aktualizr_implicit_writer |
| @@ -47,5 +46,6 @@ FILES_${PN}_class-target = " \ | |||
| 47 | " | 46 | " |
| 48 | FILES_${PN}_class-native = " \ | 47 | FILES_${PN}_class-native = " \ |
| 49 | ${bindir}/aktualizr_implicit_writer \ | 48 | ${bindir}/aktualizr_implicit_writer \ |
| 49 | ${bindir}/garage-deploy \ | ||
| 50 | ${bindir}/garage-push \ | 50 | ${bindir}/garage-push \ |
| 51 | " | 51 | " |
diff --git a/recipes-sota/aktualizr/files/sota_hsm_test.toml b/recipes-sota/aktualizr/files/sota_hsm_test.toml index 1317914..28aefc2 100644 --- a/recipes-sota/aktualizr/files/sota_hsm_test.toml +++ b/recipes-sota/aktualizr/files/sota_hsm_test.toml | |||
| @@ -12,6 +12,7 @@ pass = "1234" | |||
| 12 | 12 | ||
| 13 | [uptane] | 13 | [uptane] |
| 14 | metadata_path = "/var/sota/metadata" | 14 | metadata_path = "/var/sota/metadata" |
| 15 | private_key_path = "ecukey.der" | 15 | key_source = "pkcs11" |
| 16 | public_key_path = "ecukey.pub" | 16 | private_key_path = "03" |
| 17 | public_key_path = "03" | ||
| 17 | 18 | ||
diff --git a/recipes-sota/garage-sign/garage-sign.bb b/recipes-sota/garage-sign/garage-sign.bb index ccd7299..d5388bc 100644 --- a/recipes-sota/garage-sign/garage-sign.bb +++ b/recipes-sota/garage-sign/garage-sign.bb | |||
| @@ -6,14 +6,14 @@ LICENSE = "CLOSED" | |||
| 6 | LIC_FILES_CHKSUM = "file://${S}/docs/LICENSE;md5=3025e77db7bd3f1d616b3ffd11d54c94" | 6 | LIC_FILES_CHKSUM = "file://${S}/docs/LICENSE;md5=3025e77db7bd3f1d616b3ffd11d54c94" |
| 7 | DEPENDS = "" | 7 | DEPENDS = "" |
| 8 | 8 | ||
| 9 | PV = "0.2.0-6-g6af6ecd" | 9 | PV = "0.2.0-35-g0544c33" |
| 10 | 10 | ||
| 11 | SRC_URI = " \ | 11 | SRC_URI = " \ |
| 12 | https://ats-tuf-cli-releases.s3-eu-central-1.amazonaws.com/cli-${PV}.tgz \ | 12 | https://ats-tuf-cli-releases.s3-eu-central-1.amazonaws.com/cli-${PV}.tgz \ |
| 13 | " | 13 | " |
| 14 | 14 | ||
| 15 | SRC_URI[md5sum] = "39941607ddef3a93476e267ad7bf6280" | 15 | SRC_URI[md5sum] = "1546e06d1e747f67aee5ed7096bf1c74" |
| 16 | SRC_URI[sha256sum] = "fbd2ea56f21341146844b02837377b08e63a3e361079e2c65142c2ed881c3b5d" | 16 | SRC_URI[sha256sum] = "1432348bca8ca5ad75df1218f348f480d429d7509d6454deb6e16ff31c5e08fc" |
| 17 | 17 | ||
| 18 | S = "${WORKDIR}/${BPN}" | 18 | S = "${WORKDIR}/${BPN}" |
| 19 | 19 | ||
diff --git a/recipes-support/ca-certificates/ca-certificates_%.bbappend b/recipes-support/ca-certificates/ca-certificates_%.bbappend new file mode 100644 index 0000000..afaadfd --- /dev/null +++ b/recipes-support/ca-certificates/ca-certificates_%.bbappend | |||
| @@ -0,0 +1 @@ | |||
| SYSROOT_DIRS += "/etc" | |||
diff --git a/recipes-support/libp11/libp11_0.4.7.bb b/recipes-support/libp11/libp11_0.4.7.bb new file mode 100644 index 0000000..7d77e90 --- /dev/null +++ b/recipes-support/libp11/libp11_0.4.7.bb | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | SUMMARY = "Library for using PKCS" | ||
| 2 | DESCRIPTION = "\ | ||
| 3 | Libp11 is a library implementing a small layer on top of PKCS \ | ||
| 4 | make using PKCS" | ||
| 5 | HOMEPAGE = "http://www.opensc-project.org/libp11" | ||
| 6 | SECTION = "Development/Libraries" | ||
| 7 | LICENSE = "LGPLv2+" | ||
| 8 | LIC_FILES_CHKSUM = "file://COPYING;md5=fad9b3332be894bab9bc501572864b29" | ||
| 9 | DEPENDS = "libtool openssl" | ||
| 10 | |||
| 11 | SRC_URI = "git://github.com/OpenSC/libp11.git" | ||
| 12 | SRCREV = "da725ab727342083478150a203a3c80c4551feb4" | ||
| 13 | |||
| 14 | S = "${WORKDIR}/git" | ||
| 15 | |||
| 16 | inherit autotools pkgconfig | ||
| 17 | |||
| 18 | # Currently, Makefile dependencies are incorrectly defined which causes build errors | ||
| 19 | # The number of jobs is high | ||
| 20 | # See https://github.com/OpenSC/libp11/issues/94 | ||
| 21 | PARALLEL_MAKE = "" | ||
| 22 | EXTRA_OECONF = "--disable-static" | ||
| 23 | |||
| 24 | do_install_append () { | ||
| 25 | rm -rf ${D}${libdir}/*.la | ||
| 26 | rm -rf ${D}${docdir}/${BPN} | ||
| 27 | } | ||
| 28 | |||
| 29 | FILES_${PN} = "${libdir}/engines/pkcs11.so \ | ||
| 30 | ${libdir}/engines/libpkcs11${SOLIBS} \ | ||
| 31 | ${libdir}/libp11${SOLIBS}" | ||
| 32 | |||
| 33 | FILES_${PN}-dev = " \ | ||
| 34 | ${libdir}/engines/libpkcs11${SOLIBSDEV} \ | ||
| 35 | ${libdir}/libp11${SOLIBSDEV} \ | ||
| 36 | ${libdir}/pkgconfig/libp11.pc \ | ||
| 37 | /usr/include" | ||
diff --git a/scripts/lib/wic/plugins/source/otaimage.py b/scripts/lib/wic/plugins/source/otaimage.py index eef0bb4..26cfb10 100644 --- a/scripts/lib/wic/plugins/source/otaimage.py +++ b/scripts/lib/wic/plugins/source/otaimage.py | |||
| @@ -19,10 +19,12 @@ import logging | |||
| 19 | import os | 19 | import os |
| 20 | import sys | 20 | import sys |
| 21 | 21 | ||
| 22 | from wic.pluginbase import SourcePlugin | 22 | from wic.plugins.source.rawcopy import RawCopyPlugin |
| 23 | from wic.utils.misc import get_bitbake_var | 23 | from wic.utils.misc import get_bitbake_var |
| 24 | 24 | ||
| 25 | class OTAImagePlugin(SourcePlugin): | 25 | logger = logging.getLogger('wic') |
| 26 | |||
| 27 | class OTAImagePlugin(RawCopyPlugin): | ||
| 26 | """ | 28 | """ |
| 27 | Add an already existing filesystem image to the partition layout. | 29 | Add an already existing filesystem image to the partition layout. |
| 28 | """ | 30 | """ |
| @@ -30,25 +32,6 @@ class OTAImagePlugin(SourcePlugin): | |||
| 30 | name = 'otaimage' | 32 | name = 'otaimage' |
| 31 | 33 | ||
| 32 | @classmethod | 34 | @classmethod |
| 33 | def do_install_disk(cls, disk, disk_name, cr, workdir, oe_builddir, | ||
| 34 | bootimg_dir, kernel_dir, native_sysroot): | ||
| 35 | """ | ||
| 36 | Called after all partitions have been prepared and assembled into a | ||
| 37 | disk image. Do nothing. | ||
| 38 | """ | ||
| 39 | pass | ||
| 40 | |||
| 41 | @classmethod | ||
| 42 | def do_configure_partition(cls, part, source_params, cr, cr_workdir, | ||
| 43 | oe_builddir, bootimg_dir, kernel_dir, | ||
| 44 | native_sysroot): | ||
| 45 | """ | ||
| 46 | Called before do_prepare_partition(). Possibly prepare | ||
| 47 | configuration files of some sort. | ||
| 48 | """ | ||
| 49 | pass | ||
| 50 | |||
| 51 | @classmethod | ||
| 52 | def do_prepare_partition(cls, part, source_params, cr, cr_workdir, | 35 | def do_prepare_partition(cls, part, source_params, cr, cr_workdir, |
| 53 | oe_builddir, bootimg_dir, kernel_dir, | 36 | oe_builddir, bootimg_dir, kernel_dir, |
| 54 | rootfs_dir, native_sysroot): | 37 | rootfs_dir, native_sysroot): |
| @@ -65,5 +48,10 @@ class OTAImagePlugin(SourcePlugin): | |||
| 65 | src = bootimg_dir + "/" + get_bitbake_var("IMAGE_LINK_NAME") + ".otaimg" | 48 | src = bootimg_dir + "/" + get_bitbake_var("IMAGE_LINK_NAME") + ".otaimg" |
| 66 | 49 | ||
| 67 | logger.debug('Preparing partition using image %s' % (src)) | 50 | logger.debug('Preparing partition using image %s' % (src)) |
| 68 | part.prepare_rootfs_from_fs_image(cr_workdir, src, "") | 51 | source_params['file'] = src |
| 52 | |||
| 53 | super(OTAImagePlugin, cls).do_prepare_partition(part, source_params, | ||
| 54 | cr, cr_workdir, oe_builddir, | ||
| 55 | bootimg_dir, kernel_dir, | ||
| 56 | rootfs_dir, native_sysroot) | ||
| 69 | 57 | ||
diff --git a/scripts/qemucommand.py b/scripts/qemucommand.py new file mode 100644 index 0000000..82a9540 --- /dev/null +++ b/scripts/qemucommand.py | |||
| @@ -0,0 +1,127 @@ | |||
| 1 | from os.path import exists, join, realpath, abspath | ||
| 2 | from os import listdir | ||
| 3 | import random | ||
| 4 | import socket | ||
| 5 | from subprocess import check_output, CalledProcessError | ||
| 6 | |||
| 7 | EXTENSIONS = { | ||
| 8 | 'intel-corei7-64': 'wic', | ||
| 9 | 'qemux86-64': 'otaimg' | ||
| 10 | } | ||
| 11 | |||
| 12 | |||
| 13 | def find_local_port(start_port): | ||
| 14 | """" | ||
| 15 | Find the next free TCP port after 'start_port'. | ||
| 16 | """ | ||
| 17 | |||
| 18 | for port in range(start_port, start_port + 10): | ||
| 19 | try: | ||
| 20 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| 21 | s.bind(('', port)) | ||
| 22 | return port | ||
| 23 | except socket.error: | ||
| 24 | print("Skipping port %d" % port) | ||
| 25 | finally: | ||
| 26 | s.close() | ||
| 27 | raise Exception("Could not find a free TCP port") | ||
| 28 | |||
| 29 | |||
| 30 | def random_mac(): | ||
| 31 | """Return a random Ethernet MAC address | ||
| 32 | @link https://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml#ethernet-numbers-2 | ||
| 33 | """ | ||
| 34 | head = "ca:fe:" | ||
| 35 | hex_digits = '0123456789abcdef' | ||
| 36 | tail = ':'.join([random.choice(hex_digits) + random.choice(hex_digits) for _ in range(4)]) | ||
| 37 | return head + tail | ||
| 38 | |||
| 39 | |||
| 40 | class QemuCommand(object): | ||
| 41 | def __init__(self, args): | ||
| 42 | if args.machine: | ||
| 43 | self.machine = args.machine | ||
| 44 | else: | ||
| 45 | machines = listdir(args.dir) | ||
| 46 | if len(machines) == 1: | ||
| 47 | self.machine = machines[0] | ||
| 48 | else: | ||
| 49 | raise ValueError("Could not autodetect machine type from %s" % args.dir) | ||
| 50 | if args.efi: | ||
| 51 | self.bios = 'OVMF.fd' | ||
| 52 | else: | ||
| 53 | uboot = abspath(join(args.dir, self.machine, 'u-boot-qemux86-64.rom')) | ||
| 54 | if not exists(uboot): | ||
| 55 | raise ValueError("U-Boot image %s does not exist" % uboot) | ||
| 56 | self.bios = uboot | ||
| 57 | if exists(args.imagename): | ||
| 58 | image = args.imagename | ||
| 59 | else: | ||
| 60 | ext = EXTENSIONS.get(self.machine, 'wic') | ||
| 61 | image = join(args.dir, self.machine, '%s-%s.%s' % (args.imagename, self.machine, ext)) | ||
| 62 | self.image = realpath(image) | ||
| 63 | if not exists(self.image): | ||
| 64 | raise ValueError("OS image %s does not exist" % self.image) | ||
| 65 | if args.mac: | ||
| 66 | self.mac_address = args.mac | ||
| 67 | else: | ||
| 68 | self.mac_address = random_mac() | ||
| 69 | self.serial_port = find_local_port(8990) | ||
| 70 | self.ssh_port = find_local_port(2222) | ||
| 71 | if args.kvm is None: | ||
| 72 | # Autodetect KVM using 'kvm-ok' | ||
| 73 | try: | ||
| 74 | check_output(['kvm-ok']) | ||
| 75 | self.kvm = True | ||
| 76 | except CalledProcessError: | ||
| 77 | self.kvm = False | ||
| 78 | else: | ||
| 79 | self.kvm = args.kvm | ||
| 80 | self.gui = not args.no_gui | ||
| 81 | self.gdb = args.gdb | ||
| 82 | self.pcap = args.pcap | ||
| 83 | self.overlay = args.overlay | ||
| 84 | |||
| 85 | def command_line(self): | ||
| 86 | netuser = 'user,hostfwd=tcp:0.0.0.0:%d-:22,restrict=off' % self.ssh_port | ||
| 87 | if self.gdb: | ||
| 88 | netuser += ',hostfwd=tcp:0.0.0.0:2159-:2159' | ||
| 89 | cmdline = [ | ||
| 90 | "qemu-system-x86_64", | ||
| 91 | "-bios", self.bios | ||
| 92 | ] | ||
| 93 | if not self.overlay: | ||
| 94 | cmdline += ["-drive", "file=%s,if=ide,format=raw,snapshot=on" % self.image] | ||
| 95 | cmdline += [ | ||
| 96 | "-serial", "tcp:127.0.0.1:%d,server,nowait" % self.serial_port, | ||
| 97 | "-m", "1G", | ||
| 98 | "-usb", | ||
| 99 | "-usbdevice", "tablet", | ||
| 100 | "-show-cursor", | ||
| 101 | "-vga", "std", | ||
| 102 | "-net", netuser, | ||
| 103 | "-net", "nic,macaddr=%s" % self.mac_address | ||
| 104 | ] | ||
| 105 | if self.pcap: | ||
| 106 | cmdline += ['-net', 'dump,file=' + self.pcap] | ||
| 107 | if self.gui: | ||
| 108 | cmdline += ["-serial", "stdio"] | ||
| 109 | else: | ||
| 110 | cmdline.append('-nographic') | ||
| 111 | if self.kvm: | ||
| 112 | cmdline.append('-enable-kvm') | ||
| 113 | else: | ||
| 114 | cmdline += ['-cpu', 'Haswell'] | ||
| 115 | if self.overlay: | ||
| 116 | cmdline.append(self.overlay) | ||
| 117 | return cmdline | ||
| 118 | |||
| 119 | def img_command_line(self): | ||
| 120 | cmdline = [ | ||
| 121 | "qemu-img", "create", | ||
| 122 | "-o", "backing_file=%s" % self.image, | ||
| 123 | "-f", "qcow2", | ||
| 124 | self.overlay] | ||
| 125 | return cmdline | ||
| 126 | |||
| 127 | |||
diff --git a/scripts/run-qemu-ota b/scripts/run-qemu-ota index 5334814..56e4fbc 100755 --- a/scripts/run-qemu-ota +++ b/scripts/run-qemu-ota | |||
| @@ -2,126 +2,12 @@ | |||
| 2 | 2 | ||
| 3 | from argparse import ArgumentParser | 3 | from argparse import ArgumentParser |
| 4 | from subprocess import Popen | 4 | from subprocess import Popen |
| 5 | from os.path import exists, join, realpath | 5 | from os.path import exists |
| 6 | from os import listdir | ||
| 7 | import random | ||
| 8 | import sys | 6 | import sys |
| 9 | import socket | 7 | from qemucommand import QemuCommand |
| 10 | 8 | ||
| 11 | DEFAULT_DIR = 'tmp/deploy/images' | 9 | DEFAULT_DIR = 'tmp/deploy/images' |
| 12 | 10 | ||
| 13 | EXTENSIONS = { | ||
| 14 | 'intel-corei7-64': 'wic', | ||
| 15 | 'qemux86-64': 'otaimg' | ||
| 16 | } | ||
| 17 | |||
| 18 | |||
| 19 | def find_local_port(start_port): | ||
| 20 | """" | ||
| 21 | Find the next free TCP port after 'start_port'. | ||
| 22 | """ | ||
| 23 | |||
| 24 | for port in range(start_port, start_port + 10): | ||
| 25 | try: | ||
| 26 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| 27 | s.bind(('', port)) | ||
| 28 | return port | ||
| 29 | except socket.error: | ||
| 30 | print("Skipping port %d" % port) | ||
| 31 | finally: | ||
| 32 | s.close() | ||
| 33 | raise Exception("Could not find a free TCP port") | ||
| 34 | |||
| 35 | |||
| 36 | def random_mac(): | ||
| 37 | """Return a random Ethernet MAC address | ||
| 38 | @link https://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml#ethernet-numbers-2 | ||
| 39 | """ | ||
| 40 | head = "ca:fe:" | ||
| 41 | hex_digits = '0123456789abcdef' | ||
| 42 | tail = ':'.join([random.choice(hex_digits) + random.choice(hex_digits) for _ in range(4)]) | ||
| 43 | return head + tail | ||
| 44 | |||
| 45 | |||
| 46 | class QemuCommand(object): | ||
| 47 | def __init__(self, args): | ||
| 48 | if args.machine: | ||
| 49 | self.machine = args.machine | ||
| 50 | else: | ||
| 51 | machines = listdir(args.dir) | ||
| 52 | if len(machines) == 1: | ||
| 53 | self.machine = machines[0] | ||
| 54 | else: | ||
| 55 | raise ValueError("Could not autodetect machine type from %s" % args.dir) | ||
| 56 | if args.efi: | ||
| 57 | self.bios = 'OVMF.fd' | ||
| 58 | else: | ||
| 59 | uboot = join(args.dir, self.machine, 'u-boot-qemux86-64.rom') | ||
| 60 | if not exists(uboot): | ||
| 61 | raise ValueError("U-Boot image %s does not exist" % uboot) | ||
| 62 | self.bios = uboot | ||
| 63 | if exists(args.imagename): | ||
| 64 | image = args.imagename | ||
| 65 | else: | ||
| 66 | ext = EXTENSIONS.get(self.machine, 'wic') | ||
| 67 | image = join(args.dir, self.machine, '%s-%s.%s' % (args.imagename, self.machine, ext)) | ||
| 68 | self.image = realpath(image) | ||
| 69 | if not exists(self.image): | ||
| 70 | raise ValueError("OS image %s does not exist" % self.image) | ||
| 71 | if args.mac: | ||
| 72 | self.mac_address = args.mac | ||
| 73 | else: | ||
| 74 | self.mac_address = random_mac() | ||
| 75 | self.serial_port = find_local_port(8990) | ||
| 76 | self.ssh_port = find_local_port(2222) | ||
| 77 | self.kvm = not args.no_kvm | ||
| 78 | self.gui = not args.no_gui | ||
| 79 | self.gdb = args.gdb | ||
| 80 | self.pcap = args.pcap | ||
| 81 | self.overlay = args.overlay | ||
| 82 | |||
| 83 | def command_line(self): | ||
| 84 | netuser = 'user,hostfwd=tcp:0.0.0.0:%d-:22,restrict=off' % self.ssh_port | ||
| 85 | if self.gdb: | ||
| 86 | netuser += ',hostfwd=tcp:0.0.0.0:2159-:2159' | ||
| 87 | cmdline = [ | ||
| 88 | "qemu-system-x86_64", | ||
| 89 | "-bios", self.bios | ||
| 90 | ] | ||
| 91 | if not self.overlay: | ||
| 92 | cmdline += ["-drive", "file=%s,if=ide,format=raw,snapshot=on" % self.image] | ||
| 93 | cmdline += [ | ||
| 94 | "-serial", "tcp:127.0.0.1:%d,server,nowait" % self.serial_port, | ||
| 95 | "-m", "1G", | ||
| 96 | "-usb", | ||
| 97 | "-usbdevice", "tablet", | ||
| 98 | "-show-cursor", | ||
| 99 | "-vga", "std", | ||
| 100 | "-net", netuser, | ||
| 101 | "-net", "nic,macaddr=%s" % self.mac_address | ||
| 102 | ] | ||
| 103 | if self.pcap: | ||
| 104 | cmdline += ['-net', 'dump,file=' + self.pcap] | ||
| 105 | if self.gui: | ||
| 106 | cmdline += ["-serial", "stdio"] | ||
| 107 | else: | ||
| 108 | cmdline.append('-nographic') | ||
| 109 | if self.kvm: | ||
| 110 | cmdline.append('-enable-kvm') | ||
| 111 | else: | ||
| 112 | cmdline += ['-cpu', 'Haswell'] | ||
| 113 | if self.overlay: | ||
| 114 | cmdline.append(self.overlay) | ||
| 115 | return cmdline | ||
| 116 | |||
| 117 | def img_command_line(self): | ||
| 118 | cmdline = [ | ||
| 119 | "qemu-img", "create", | ||
| 120 | "-o", "backing_file=%s" % self.image, | ||
| 121 | "-f", "qcow2", | ||
| 122 | self.overlay] | ||
| 123 | return cmdline | ||
| 124 | |||
| 125 | 11 | ||
| 126 | def main(): | 12 | def main(): |
| 127 | parser = ArgumentParser(description='Run meta-updater image in qemu') | 13 | parser = ArgumentParser(description='Run meta-updater image in qemu') |
| @@ -135,11 +21,18 @@ def main(): | |||
| 135 | 'OSTREE_BOOTLOADER = "grub" and OVMF.fd firmware to be installed (try "apt install ovmf")', | 21 | 'OSTREE_BOOTLOADER = "grub" and OVMF.fd firmware to be installed (try "apt install ovmf")', |
| 136 | action='store_true') | 22 | action='store_true') |
| 137 | parser.add_argument('--machine', default=None, help="Target MACHINE") | 23 | parser.add_argument('--machine', default=None, help="Target MACHINE") |
| 138 | parser.add_argument('--no-kvm', help='Disable KVM in QEMU', action='store_true') | 24 | kvm_group = parser.add_argument_group() |
| 25 | kvm_group.add_argument('--force-kvm', help='Force use of KVM (default is to autodetect)', | ||
| 26 | dest='kvm', action='store_true', default=None) | ||
| 27 | kvm_group.add_argument('--no-kvm', help='Disable KVM in QEMU', | ||
| 28 | dest='kvm', action='store_false') | ||
| 139 | parser.add_argument('--no-gui', help='Disable GUI', action='store_true') | 29 | parser.add_argument('--no-gui', help='Disable GUI', action='store_true') |
| 140 | parser.add_argument('--gdb', help='Export gdbserver port 2159 from the image', action='store_true') | 30 | parser.add_argument('--gdb', help='Export gdbserver port 2159 from the image', action='store_true') |
| 141 | parser.add_argument('--pcap', default=None, help='Dump all network traffic') | 31 | parser.add_argument('--pcap', default=None, help='Dump all network traffic') |
| 142 | parser.add_argument('-o', '--overlay', type=str, metavar='file.cow', help='Use an overlay storage image file. Will be created if it does not exist. This option lets you have a persistent image without modifying the underlying image file, permitting multiple different persistent machines.') | 32 | parser.add_argument('-o', '--overlay', type=str, metavar='file.cow', |
| 33 | help='Use an overlay storage image file. Will be created if it does not exist. ' + | ||
| 34 | 'This option lets you have a persistent image without modifying the underlying image ' + | ||
| 35 | 'file, permitting multiple different persistent machines.') | ||
| 143 | parser.add_argument('-n', '--dry-run', help='Print qemu command line rather then run it', action='store_true') | 36 | parser.add_argument('-n', '--dry-run', help='Print qemu command line rather then run it', action='store_true') |
| 144 | args = parser.parse_args() | 37 | args = parser.parse_args() |
| 145 | try: | 38 | try: |
| @@ -161,7 +54,7 @@ def main(): | |||
| 161 | if args.dry_run: | 54 | if args.dry_run: |
| 162 | print(" ".join(img_cmdline)) | 55 | print(" ".join(img_cmdline)) |
| 163 | else: | 56 | else: |
| 164 | Popen(img_cmdline) | 57 | Popen(img_cmdline).wait() |
| 165 | 58 | ||
| 166 | if args.dry_run: | 59 | if args.dry_run: |
| 167 | print(" ".join(cmdline)) | 60 | print(" ".join(cmdline)) |
