#!/bin/sh # EdgeOne Provisioning bootstrapper (ADR 0001, layer 1). POSIX sh. # # Detects /etc/os-release and `dpkg --print-architecture`, enforces the OS # matrix, adds the Microsoft and BlackWell apt repositories with signed-by # keyrings (each key's fingerprint is checked against the embedded one before # any keyring is written) and installs edgeone-provisioning. It never touches # device identity and never enables backports. Safe to re-run. # # Usage and exit codes: usage() below (sh install.sh --help). set -eu # --- Release-time constants -------------------------------------------------- # Rendered by scripts/render-install-sh.sh. While they hold the @...@ # placeholders the script refuses to run (exit 78). The BlackWell host, suite # and key are assigned by blackwell-infra (ADR 0001, "Hosting"). BLACKWELL_APT_URL='https://packages.dev.getblackwell.systems' BLACKWELL_SUITE='stable/' BLACKWELL_KEY_FPR='9E5EC3C7C9061FFE2CC2C8F10A60707D60913E73' # Under the base URL: the armored repository key and this script's signature. # Part of the consumer contract fixed by amendment once the host is assigned. BLACKWELL_KEY_FILE='archive-key.asc' BLACKWELL_SIG_FILE='install.sh.asc' # --- Microsoft (packages.microsoft.com) ----------------------------------------- # Layout and keys verified 2026-10-17 against # https://learn.microsoft.com/en-us/linux/packages (key fingerprints, config URIs) # https://packages.microsoft.com/config/debian/12/prod.list # https://packages.microsoft.com/config/ubuntu/24.04/prod.list # ("deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] # https://packages.microsoft.com///prod main"); the # Debian 12 and Ubuntu 24.04 Release.gpg are issued by EB3E94ADBE1229CF. # microsoft.asc signs repositories created before May 2025 (Debian 12, Ubuntu # 24.04); microsoft-2025.asc those created after April 2025 (Debian 13 and # later, so the preview OSes). MICROSOFT_URL='https://packages.microsoft.com' MICROSOFT_KEY_FPR='BC528686B50D79E339D3721CEB3E94ADBE1229CF' MICROSOFT_2025_KEY_FPR='AA86F75E427A19DD33346403EE4D7792F748182B' PACKAGE=edgeone-provisioning KEYRING_DIR=/usr/share/keyrings SOURCES_DIR=/etc/apt/sources.list.d OS_RELEASE=/etc/os-release UPDATE_ATTEMPTS=4 # Never fails: a closed stderr must not turn a documented exit code into 1. log() { printf 'install.sh: %s\n' "$*" >&2 || :; } die() { rc=$1 shift log "$*" exit "$rc" } # A heredoc, not this file's comments, so --help also works under # `curl ... | sh -s -- --help`, where there is no file to read. usage() { cat <<'EOF' Usage: sh install.sh [options] Installs edgeone-provisioning from the signed BlackWell and Microsoft apt repositories on Debian 12 or Ubuntu 24.04 (amd64, arm64). Never touches device identity, never enables backports. Safe to re-run. Run as root. Options: --version VER install edgeone-provisioning=VER; an older VER than the installed one is a deliberate downgrade (rollback) --mirror URL BlackWell repository base URL; keys are still checked against the embedded fingerprints --microsoft-mirror URL packages.microsoft.com base URL; same check --allow-http-mirror accept a plain-http mirror URL (keys and install.sh.asc then travel over http too; the fingerprints still bind) --allow-unsupported-os accept the preview OSes (Debian 13, Ubuntu 26.04); forwarded to edgeonectl provision with --provision --lock-timeout SECONDS total budget for waiting on the dpkg/apt locks over the whole run, not per wait (default 120) --non-interactive never prompt (the installer itself never does); excludes --provision --verify-only check this file against install.sh.asc with the embedded BlackWell key and exit (a convenience: it is not a trust anchor for a script of unknown origin) --provision after installing, exec `edgeonectl provision`. Needs a terminal on stdout. Token input, forwarded unread: --token-file PATH read by edgeonectl --token-stdin stdin must be a pipe, not a terminal; run install.sh from a file (under `curl | sh`, stdin carries the script) (neither) hidden prompt; stdin must be a terminal also forwarded: --environment ENV, --allow-unsupported-os -h, --help this help Environment: EDGEONE_PROVISIONING_MIRROR as --mirror EDGEONE_PROVISIONING_MICROSOFT_MIRROR as --microsoft-mirror EDGEONE_PROVISIONING_LOCK_TIMEOUT as --lock-timeout EDGEONE_PROVISIONING_ALLOW_HTTP_MIRROR 1 as --allow-http-mirror (0 or 1) Flags override the environment. Exit codes (sysexits, ADR 0006): 0 success (also: pinned --version already installed, nothing to do) 64 usage: unknown or malformed option; token flags without --provision; --provision without a terminal on stdout, with --token-stdin on a terminal or under `curl | sh`, or with the prompt and no terminal on stdin; plain-http mirror without --allow-http-mirror 69 unavailable: download, apt-get update/install, or a local operation (temporary directory, writing keyrings or sources) failed 75 temporary: the dpkg/apt lock stayed held past --lock-timeout; re-run 77 not root 78 configuration (operator action needed, retrying will not help): unsupported OS or architecture; unrendered or tampered install.sh; a repository key with the wrong fingerprint; a Microsoft or BlackWell apt source not written by install.sh; docker.io/docker-ce/docker-ee/ docker-engine installed (moby-engine replaces them); moby-engine would remove packages; half-configured dpkg (run dpkg --configure -a, then re-run install.sh) EOF } usage_error() { log "$*" log "see: sh install.sh --help" exit 64 } TMP= cleanup() { [ -z "$TMP" ] || rm -rf -- "$TMP"; } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM # --- Arguments --------------------------------------------------------------- version= mirror=${EDGEONE_PROVISIONING_MIRROR:-} ms_mirror=${EDGEONE_PROVISIONING_MICROSOFT_MIRROR:-} lock_timeout=${EDGEONE_PROVISIONING_LOCK_TIMEOUT:-120} allow_unsupported=0 verify_only=0 provision=0 non_interactive=0 case ${EDGEONE_PROVISIONING_ALLOW_HTTP_MIRROR:-0} in 0) allow_http=0 ;; 1) allow_http=1 ;; *) usage_error "EDGEONE_PROVISIONING_ALLOW_HTTP_MIRROR must be 0 or 1" ;; esac token_inputs=0 token_mode=prompt # Arguments forwarded to `edgeonectl provision` are rotated back into "$@". n=$# while [ "$n" -gt 0 ]; do arg=$1 shift n=$((n - 1)) case $arg in -h | --help) usage exit 0 ;; --version | --mirror | --microsoft-mirror | --lock-timeout | --token-file | --environment) [ "$n" -gt 0 ] || usage_error "$arg needs a value" val=$1 shift n=$((n - 1)) case $arg in --version) version=$val ;; --mirror) mirror=$val ;; --microsoft-mirror) ms_mirror=$val ;; --lock-timeout) lock_timeout=$val ;; *) token_inputs=1 [ "$arg" != --token-file ] || token_mode='file' set -- "$@" "$arg" "$val" ;; esac ;; --token-stdin) token_inputs=1 token_mode=stdin set -- "$@" "$arg" ;; --allow-unsupported-os) allow_unsupported=1 set -- "$@" "$arg" ;; --allow-http-mirror) allow_http=1 ;; --verify-only) verify_only=1 ;; --provision) provision=1 ;; --non-interactive) non_interactive=1 ;; *) usage_error "unknown argument: $arg" ;; esac done case $lock_timeout in '' | *[!0-9]*) usage_error "--lock-timeout must be a number of seconds" ;; esac case $version in *[!0-9A-Za-z.+~:-]*) usage_error "invalid --version '$version'" ;; esac for url in "$mirror" "$ms_mirror"; do case $url in '' | http://?* | https://?*) ;; *) usage_error "mirror must be an http(s) URL: $url" ;; esac case $url in *[!A-Za-z0-9.:/_~%@+-]*) usage_error "invalid characters in mirror URL: $url" ;; esac done if [ "$token_inputs" = 1 ] && [ "$provision" = 0 ]; then usage_error "--token-file, --token-stdin and --environment are only forwarded with --provision" fi if [ "$provision" = 1 ]; then [ "$non_interactive" = 0 ] || usage_error "--provision is interactive; drop --non-interactive" [ "$verify_only" = 0 ] || usage_error "--provision and --verify-only exclude each other" # P5: the terminal is for edgeonectl's output; stdin depends on the token # input (edgeonectl refuses --token-stdin on a terminal, where it would echo). [ -t 1 ] || usage_error "--provision needs a terminal on stdout; run edgeonectl provision directly" case $token_mode in stdin) [ ! -t 0 ] || usage_error "--token-stdin reads a pipe, not a terminal: pipe the token or use --token-file / the prompt" # Under `curl | sh` stdin carries the script itself, not the token. [ -f "$0" ] || usage_error "--token-stdin needs install.sh run from a file (sh install.sh ...); use --token-file or the prompt" ;; prompt) [ -t 0 ] || usage_error "--provision without --token-file or --token-stdin prompts for the token and needs a terminal on stdin" ;; esac fi # --- Release configuration --------------------------------------------------- case $BLACKWELL_APT_URL$BLACKWELL_SUITE$BLACKWELL_KEY_FPR in *@*) die 78 "this install.sh is not configured for release (unrendered placeholders); use the published, signed install.sh" ;; esac case $BLACKWELL_KEY_FPR in *[!0-9A-F]*) die 78 "invalid embedded BlackWell key fingerprint" ;; esac [ "${#BLACKWELL_KEY_FPR}" -eq 40 ] || die 78 "invalid embedded BlackWell key fingerprint" case $BLACKWELL_SUITE in */) ;; *) die 78 "BlackWell suite must be a flat-repository path ending in /" ;; esac bw_base=${mirror:-$BLACKWELL_APT_URL} bw_base=${bw_base%/} ms_base=${ms_mirror:-$MICROSOFT_URL} ms_base=${ms_base%/} # Plain http only on request (P6): apt still checks the signed-by keyrings, but # the keys and install.sh.asc are fetched over the same transport. for url in "$bw_base" "$ms_base"; do case $url in http://*) [ "$allow_http" = 1 ] || usage_error "refusing plain-http repository URL $url; pass --allow-http-mirror (or EDGEONE_PROVISIONING_ALLOW_HTTP_MIRROR=1) to accept it" ;; esac done curl_proto='=https' [ "$allow_http" = 0 ] || curl_proto='=https,http' TMP=$(mktemp -d) || die 69 "cannot create a temporary directory (mktemp -d)" mkdir -m 0700 "$TMP/gnupg" || die 69 "cannot create $TMP/gnupg" GNUPGHOME=$TMP/gnupg export GNUPGHOME need() { for tool in "$@"; do command -v "$tool" >/dev/null 2>&1 || return 1 done } # Only keys and signatures are fetched here (small files): 30 s per try. fetch() { curl -fsSL --proto "$curl_proto" --proto-redir "$curl_proto" --connect-timeout 20 \ --max-time 30 --retry 3 -o "$2" "$1" || die 69 "cannot download $1" } # Prints the fingerprint of the only primary key in armored file $1; fails # unless the file holds exactly one primary key. key_fpr() { gpg --batch --quiet --show-keys --with-colons "$1" 2>/dev/null | awk -F: ' $1 == "pub" { n++; p = 1; next } p && $1 == "fpr" { f = $10 } { p = 0 } END { if (n != 1) exit 1; print f }' } # Downloads key $1, checks it against fingerprint $2 and leaves the binary # keyring in $3. Nothing outside $TMP is written before the check passes. fetch_verified_key() { fetch "$1" "$TMP/key.asc" got=$(key_fpr "$TMP/key.asc") || got= [ "$got" = "$2" ] || die 78 "key $1 has fingerprint ${got:-}, expected $2; refusing to use it" rm -f -- "$3" || die 69 "cannot remove $3" gpg --batch --quiet --dearmor -o "$3" "$TMP/key.asc" || die 78 "cannot dearmor $1" } # Installs $1 at $2 with mode $3 unless $2 already has that content; sets # changed=1 when it writes. changed=0 write_if_changed() { if [ -f "$2" ] && cmp -s "$1" "$2"; then chmod "$3" "$2" || die 69 "cannot chmod $2" log "unchanged: $2" return 0 fi cp -- "$1" "$2.tmp.$$" || die 69 "cannot write $2.tmp.$$" chmod "$3" "$2.tmp.$$" || die 69 "cannot chmod $2.tmp.$$" mv -f -- "$2.tmp.$$" "$2" || die 69 "cannot move $2.tmp.$$ to $2" changed=1 log "wrote: $2" } # --- --verify-only ------------------------------------------------------------- if [ "$verify_only" = 1 ]; then self=$0 [ -f "$self" ] || usage_error "--verify-only needs the script as a file: sh install.sh --verify-only" need curl gpg gpgv || die 69 "--verify-only needs curl, gpg and gpgv (apt-get install curl gpg gpgv)" fetch_verified_key "$bw_base/$BLACKWELL_KEY_FILE" "$BLACKWELL_KEY_FPR" "$TMP/blackwell.gpg" sig=$self.asc if [ ! -f "$sig" ]; then sig=$TMP/install.sh.asc fetch "$bw_base/$BLACKWELL_SIG_FILE" "$sig" fi if gpgv --keyring "$TMP/blackwell.gpg" "$sig" "$self" >"$TMP/gpgv.log" 2>&1; then log "OK: $self is signed by the BlackWell key $BLACKWELL_KEY_FPR ($sig)" exit 0 fi cat "$TMP/gpgv.log" >&2 || : die 78 "signature check FAILED for $self; do not run it" fi # --- Preconditions ------------------------------------------------------------- [ "$(id -u)" -eq 0 ] || die 77 "must run as root (sudo sh install.sh)" [ -r "$OS_RELEASE" ] || OS_RELEASE=/usr/lib/os-release [ -r "$OS_RELEASE" ] || die 78 "cannot read /etc/os-release" # shellcheck disable=SC1090 # os-release(5) is meant to be sourced os_field() { (. "$OS_RELEASE" && eval "printf '%s' \"\${$1:-}\""); } os_id=$(os_field ID) || die 78 "cannot parse $OS_RELEASE" os_ver=$(os_field VERSION_ID) || die 78 "cannot parse $OS_RELEASE" os_codename=$(os_field VERSION_CODENAME) || die 78 "cannot parse $OS_RELEASE" command -v dpkg >/dev/null 2>&1 || die 78 "dpkg not found: only Debian and Ubuntu are supported" arch=$(dpkg --print-architecture) || die 69 "dpkg --print-architecture failed" ms_key_file=microsoft.asc ms_key_fpr=$MICROSOFT_KEY_FPR case $os_id/$os_ver in debian/12 | ubuntu/24.04) ;; debian/13 | ubuntu/26.04) [ "$allow_unsupported" = 1 ] || die 78 "$os_id $os_ver is a preview OS; re-run with --allow-unsupported-os to accept it" log "preview OS $os_id $os_ver accepted (--allow-unsupported-os)" ms_key_file=microsoft-2025.asc ms_key_fpr=$MICROSOFT_2025_KEY_FPR ;; *) die 78 "unsupported OS ${os_id:-?} ${os_ver:-?}: Debian 12 and Ubuntu 24.04 are supported" ;; esac case $arch in amd64 | arm64) ;; *) die 78 "unsupported architecture $arch (amd64, arm64)" ;; esac [ -n "$os_codename" ] || die 78 "VERSION_CODENAME missing from $OS_RELEASE" case $os_codename in *[!a-z0-9]*) die 78 "invalid VERSION_CODENAME '$os_codename'" ;; esac # --- Prior state (P2): refuse rather than clobber ---------------------------------- # Prints the URIs of the apt sources in file $1, one per line, from one-line # (sources.list(5)) and deb822 entries. source_uris() { awk ' /^[[:space:]]*#/ { next } $1 == "deb" || $1 == "deb-src" { i = 2 if ($i ~ /^\[/) { while (i <= NF && $i !~ /\]$/) i++; i++ } if (i <= NF) print $i next } tolower($1) == "uris:" { for (i = 2; i <= NF; i++) print $i } ' "$1" 2>/dev/null } # Drops the scheme (also mirror+/tor+ forms) and trailing slashes. norm_uri() { printf '%s\n' "$1" | sed -e 's|^[A-Za-z0-9+.-]*://||' -e 's|/*$||'; } ms_repo=$(norm_uri "$MICROSOFT_URL/$os_id/$os_ver/prod") ms_repo_mirror=$(norm_uri "$ms_base/$os_id/$os_ver/prod") bw_repo=$(norm_uri "$BLACKWELL_APT_URL") bw_repo_mirror=$(norm_uri "$bw_base") own_header='# Written by EdgeOne Provisioning install.sh.' for f in /etc/apt/sources.list "$SOURCES_DIR"/*.list "$SOURCES_DIR"/*.sources; do [ -f "$f" ] || continue case $f in "$SOURCES_DIR/microsoft-prod.sources" | "$SOURCES_DIR/$PACKAGE.sources") # Ours by name; refuse a same-named file written by something else. head -n 1 "$f" | grep -qF "$own_header" || die 78 "$f exists but was not written by install.sh; remove it and re-run install.sh" continue ;; esac for uri in $(source_uris "$f"); do u=$(norm_uri "$uri") for r in "$ms_repo" "$ms_repo_mirror"; do case $u in "$r" | "$r"/*) die 78 "an existing Microsoft apt source is configured in $f (for example by packages-microsoft-prod); remove it and re-run install.sh" ;; esac done for r in "$bw_repo" "$bw_repo_mirror"; do case $u in "$r" | "$r"/*) die 78 "an existing BlackWell apt source is configured in $f; remove it and re-run install.sh" ;; esac done done done # moby-engine Conflicts with these; apt would remove them and stop their # containers. That is the operator's call, never ours. for pkg in docker.io docker-ce docker-ee docker-engine; do [ "$(dpkg-query -W -f '${Status}' "$pkg" 2>/dev/null)" = "install ok installed" ] || continue die 78 "moby-engine replaces $pkg; IoT Edge needs moby-engine. Remove $pkg deliberately (containers will stop) and re-run install.sh" done # --- dpkg/apt lock, dpkg state, apt-get --------------------------------------------- # A lock is held when /proc/locks lists the inode of one of the lock files # (apt and dpkg take fcntl locks, which flock(1) cannot see). lock_held() { for f in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock \ /var/cache/apt/archives/lock; do [ -e "$f" ] || continue ino=$(stat -c %i "$f") || continue if awk -v ino="$ino" '{ for (i = 1; i <= NF; i++) if ($i ~ /^[0-9a-f]+:[0-9a-f]+:[0-9]+$/) { n = split($i, a, ":"); if (a[n] == ino) found = 1 } } END { exit !found }' /proc/locks; then return 0 fi done return 1 } # --lock-timeout bounds the total time spent waiting for locks over the whole # run (before every apt attempt and inside apt), not each wait. lock_waited=0 lock_budget_exhausted() { die 75 "the dpkg/apt lock is still held after ${lock_timeout}s (another apt or unattended-upgrades run); re-run later" } wait_for_lock() { start=$(date +%s) logged=0 while lock_held; do [ $((lock_waited + $(date +%s) - start)) -lt "$lock_timeout" ] || lock_budget_exhausted [ "$logged" = 1 ] || log "waiting up to $((lock_timeout - lock_waited))s for the dpkg/apt lock" logged=1 sleep 1 done lock_waited=$((lock_waited + $(date +%s) - start)) } apt_get() { DEBIAN_FRONTEND=noninteractive apt-get -q \ -o DPkg::Lock::Timeout="$((lock_timeout - lock_waited))" "$@" } # Runs apt-get "$@" into $TMP/apt.log, waiting for the locks first. When apt # itself loses the race for a lock ("Could not get lock"), the time is charged # to the lock budget and the run starts again; it does not count as one of the # caller's attempts. Returns non-zero for any other failure. apt_try() { while :; do wait_for_lock t0=$(date +%s) if apt_get "$@" >"$TMP/apt.log" 2>&1 && ! grep -q '^E:' "$TMP/apt.log"; then return 0 fi grep -q 'Could not get lock' "$TMP/apt.log" || return 1 sleep 1 lock_waited=$((lock_waited + $(date +%s) - t0)) [ "$lock_waited" -lt "$lock_timeout" ] || lock_budget_exhausted log "apt-get $1: a dpkg/apt lock was taken by another process; waiting again" done } apt_failed() { tail -n 20 "$TMP/apt.log" >&2 || : if grep -q 'Could not get lock' "$TMP/apt.log"; then die 75 "$1: the dpkg/apt lock is held; re-run later" fi die 69 "$1 failed" } apt_update() { attempt=1 delay=2 while :; do if apt_try update; then return 0 fi [ "$attempt" -lt "$UPDATE_ATTEMPTS" ] || apt_failed "apt-get update ($attempt attempts)" log "apt-get update failed (attempt $attempt/$UPDATE_ATTEMPTS); retrying in ${delay}s" sleep "$delay" attempt=$((attempt + 1)) delay=$((delay * 2)) done } wait_for_lock # P3: an operator action, not retryable, so 78 (not 75). audit=$(dpkg --audit 2>&1) || : if [ -n "$audit" ]; then printf '%s\n' "$audit" >&2 || : die 78 "dpkg has half-configured or unfinished packages; run dpkg --configure -a, then re-run install.sh" fi if ! need curl gpg || [ ! -f /etc/ssl/certs/ca-certificates.crt ]; then log "installing prerequisites from the distribution: ca-certificates curl gpg" apt_update apt_try install -y --no-install-recommends ca-certificates curl gpg || apt_failed "apt-get install ca-certificates curl gpg" fi # --- Repositories ------------------------------------------------------------------ fetch_verified_key "$bw_base/$BLACKWELL_KEY_FILE" "$BLACKWELL_KEY_FPR" "$TMP/blackwell.gpg" fetch_verified_key "$ms_base/keys/$ms_key_file" "$ms_key_fpr" "$TMP/microsoft.gpg" mkdir -p "$KEYRING_DIR" "$SOURCES_DIR" || die 69 "cannot create $KEYRING_DIR or $SOURCES_DIR" write_if_changed "$TMP/blackwell.gpg" "$KEYRING_DIR/$PACKAGE.gpg" 0644 write_if_changed "$TMP/microsoft.gpg" "$KEYRING_DIR/microsoft-prod.gpg" 0644 cat >"$TMP/microsoft.sources" <"$TMP/blackwell.sources" </dev/null) || installed= if [ -n "$version" ] && [ "$changed" = 0 ] && [ "$installed" = "ii $version" ]; then # Pinned version already installed and repositories unchanged (PERF-2). log "$PACKAGE $version already installed; nothing to do" else apt_update # Container engine first, in its own transaction. The aziot-edge preinst exits # 1 ("No container runtime detected") unless the docker group exists, and # aziot-edge does not Depend on moby-engine, so a single # `apt-get install edgeone-provisioning` can unpack aziot-edge before # moby-engine and leave dpkg half-configured. This follows Microsoft's # documented order ("Install IoT Edge": "Install a container engine" with # `apt-get install moby-engine`, then "Install the IoT Edge runtime" with # `apt-get install aziot-edge`; verified 2026-10-18 against # https://learn.microsoft.com/en-us/azure/iot-edge/how-to-provision-single-device-linux-symmetric # ms.date 2026-02-26). Not Pre-Depends, which Debian Policy discourages. if [ "$(dpkg-query -W -f '${db:Status-Abbrev}' moby-engine 2>/dev/null)" = "ii " ]; then log "unchanged: moby-engine already installed" else # Belt and braces for P2: never let this transaction remove a package. apt_try install -s -y moby-engine || apt_failed "apt-get install -s moby-engine" removals=$(sed -n 's/^Remv \([^ ]*\).*/\1/p' "$TMP/apt.log" | tr '\n' ' ') [ -z "$removals" ] || die 78 "installing moby-engine would remove: ${removals% }; remove them deliberately (containers will stop) and re-run install.sh" log "installing moby-engine (container engine, before IoT Edge)" apt_try install -y moby-engine || apt_failed "apt-get install moby-engine" fi target=$PACKAGE${version:+=$version} log "installing $target" if [ -n "$version" ]; then # REL-10a: an explicit --version may be older than the installed one (a # rollback); apt-get -y refuses that without --allow-downgrades. apt_try install -y --allow-downgrades "$target" || apt_failed "apt-get install $target" else apt_try install -y "$target" || apt_failed "apt-get install $target" fi log "installed: $(edgeonectl --version 2>/dev/null || echo "$PACKAGE")" fi if [ "$provision" = 1 ]; then command -v edgeonectl >/dev/null 2>&1 || die 69 "edgeonectl not found after installing $PACKAGE" cleanup trap - EXIT exec edgeonectl provision "$@" fi log "next: sudo edgeonectl check && sudo edgeonectl provision"