From 69e4ec549d86ccabcb62230a9c48523e22b7f21c Mon Sep 17 00:00:00 2001 From: Artur <40683252+Artur-at-work@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:44:02 +0800 Subject: [PATCH] Add device connector to provision laptops with 24.04 using image-deploy.sh [OEX86-359] (#304) * Add noble_oemscript device-connector and updated docs. Updated image-deploy.sh script * Fix typo in docs. * Removed unused variable in image-deploy.sh * image-deploy.sh: Create default redeploy.cfg and pc_sanity.conf * device-connectors: Add oem_autoinstall device connector. Add image-deploy.sh with default config generation * Add separate keys for user-data, redeploy.cfg, authorized_keys * 1. Updated docs with oem_autoinstall 2. Added token_file to access the url * Updated README, comments, and function names. Add sample cloud-config for user_data * Fix some wording in docs --- .github/CODEOWNERS | 2 + .../data/muxpi/oem_autoinstall/README | 5 + .../muxpi/oem_autoinstall/image-deploy.sh | 263 ++++++++++++++++++ .../devices/__init__.py | 1 + .../devices/oem_autoinstall/__init__.py | 42 +++ .../oem_autoinstall/oem_autoinstall.py | 242 ++++++++++++++++ docs/.wordlist.txt | 4 + docs/reference/device-connector-types.rst | 102 +++++++ 8 files changed, 661 insertions(+) create mode 100644 device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/README create mode 100755 device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/image-deploy.sh create mode 100644 device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/__init__.py create mode 100644 device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/oem_autoinstall.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index abfeb62f..122e3fb0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,4 @@ # OEM Software Engineering device-connectors/src/testflinger_device_connectors/devices/zapper_iot @canonical/oem-swe-iot +device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall @canonical/oem-swe-x86 +device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/image-deploy.sh @canonical/oem-swe-x86 diff --git a/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/README b/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/README new file mode 100644 index 00000000..211c6e5c --- /dev/null +++ b/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/README @@ -0,0 +1,5 @@ +image-deploy.sh originally comes from: +https://git.launchpad.net/~oem-solutions-engineers/pc-enablement/+git/oem-scripts/tree/image-deploy.sh + +This script was adapted to TF agent environment. +Any url links to internal repos were removed, so the configs files should be provided with agent attachments. If optional configs are missing, they will be generated by the script itself. \ No newline at end of file diff --git a/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/image-deploy.sh b/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/image-deploy.sh new file mode 100755 index 00000000..e1548d6e --- /dev/null +++ b/device-connectors/src/testflinger_device_connectors/data/muxpi/oem_autoinstall/image-deploy.sh @@ -0,0 +1,263 @@ +#!/bin/bash + +exec 2>&1 +set -euox pipefail +# This script was adapted to testflinger agent environment and used +# to provision OEM devices with Ubuntu Noble images +# Downloading ISO is not supported, because agent is in charge of ISO download +# + +usage() +{ +cat < ... +Options: + -h|--help The manual of the script + --iso ISO file path to be deployed on the target + -u|--user The user of the target, default ubuntu + -o|--timeout The timeout for doing the deployment, default 3600 seconds +EOF +} + +if [ $# -lt 3 ]; then + usage + exit +fi + +TARGET_USER="ubuntu" +TARGET_IPS=() +ISO_PATH= +ISO= +STORE_PART="" +TIMEOUT=3600 +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" +SSH="ssh $SSH_OPTS" +SCP="scp $SSH_OPTS" + +if [ ! -d "$HOME/.cache/oem-scripts" ]; then + mkdir -p "$HOME/.cache/oem-scripts" +fi + +create_redeploy_cfg() { + # generates grub.cfg file to start the redeployment + local filename=$1 + + cat < "$filename" +set timeout=3 + +loadfont unicode + +set menu_color_normal=white/black +set menu_color_highlight=black/light-gray + +if [ -s (\$root)/boot/grub/theme/theme.cfg ]; then + source (\$root)/boot/grub/theme/theme.cfg +fi + +menuentry "Start redeployment" { + set gfxpayload=keep + linux /casper/vmlinuz layerfs-path=minimal.standard.live.hotfix.squashfs nopersistent ds=nocloud\;s=/cdrom/cloud-configs/redeploy --- quiet splash nomodeset modprobe.blacklist=nouveau nouveau.modeset=0 autoinstall rp-partuuid=RP_PARTUUID + initrd /casper/initrd +} +menuentry "Start normal reset installation" { + set gfxpayload=keep + linux /casper/vmlinuz layerfs-path=minimal.standard.live.hotfix.squashfs nopersistent ds=nocloud\;s=/cdrom/cloud-configs/reset-media --- quiet splash nomodeset modprobe.blacklist=nouveau nouveau.modeset=0 + initrd /casper/initrd +} +grub_platform +if [ "\$grub_platform" = "efi" ]; then +menuentry 'UEFI firmware settings' { + fwsetup +} +fi + +EOL + + echo "'$filename' was generated with default content." +} + +create_sshd_conf() { + # generates config to enable the exported authorized_keys file + local filename=$1 + + cat < "$filename" +AuthorizedKeysFile .ssh/authorized_keys /etc/ssh/authorized_keys +EOL + + echo "'$filename' was generated with default content." +} + + create_meta_data() { + local filename=$1 + # currently meta-data is an empty file, but it's required by cloud-init + touch "$filename" + } + +OPTS="$(getopt -o u:o:l: --long iso:,user:,timeout:,local-config: -n 'image-deploy.sh' -- "$@")" +eval set -- "${OPTS}" +while :; do + case "$1" in + ('-h'|'--help') + usage + exit;; + ('--iso') + ISO_PATH="$2" + ISO=$(basename "$ISO_PATH") + shift 2;; + ('-u'|'--user') + TARGET_USER="$2" + shift 2;; + ('-o'|'--timeout') + TIMEOUT="$2" + shift 2;; + ('-l'|'--local-config') + CONFIG_REPO_PATH="$2" + IS_LOCAL_CONFIG="TRUE" + shift 2;; + ('--') shift; break ;; + (*) break ;; + esac +done + +if [ ! -f "$ISO_PATH" ]; then + echo "No designated ISO file" + exit +fi + +read -ra TARGET_IPS <<< "$@" + +for addr in "${TARGET_IPS[@]}"; +do + # Clear the known host + if [ -f "$HOME/.ssh/known_hosts" ]; then + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$addr" + fi + + # Find the partitions + while read -r name fstype mountpoint; + do + echo "$name,$fstype,$mountpoint" + if [ "$fstype" = "ext4" ]; then + if [ "$mountpoint" = "/home/$TARGET_USER" ] || [ "$mountpoint" = "/" ]; then + STORE_PART="/dev/$name" + break + fi + fi + done < <($SSH "$TARGET_USER"@"$addr" -- lsblk -n -l -o NAME,FSTYPE,MOUNTPOINT) + + if [ -z "$STORE_PART" ]; then + echo "Can't find partition to store ISO on target $addr" + exit + fi + RESET_PART="${STORE_PART:0:-1}2" + RESET_PARTUUID=$($SSH "$TARGET_USER"@"$addr" -- lsblk -n -o PARTUUID "$RESET_PART") + EFI_PART="${STORE_PART:0:-1}1" + + # Copy ISO to the target + $SCP "$ISO_PATH" "$TARGET_USER"@"$addr":/home/"$TARGET_USER" + + # Copy cloud-config redeploy to the target + $SSH "$TARGET_USER"@"$addr" -- mkdir -p /home/"$TARGET_USER"/redeploy/cloud-configs/redeploy + $SSH "$TARGET_USER"@"$addr" -- mkdir -p /home/"$TARGET_USER"/redeploy/cloud-configs/grub + + if [ -n "$IS_LOCAL_CONFIG" ]; then + # configs in current dir are without folder structure + $SCP "$CONFIG_REPO_PATH"/user-data "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/redeploy/ + + if [ ! -r "$CONFIG_REPO_PATH"/meta-data ]; then + create_meta_data "$CONFIG_REPO_PATH"/meta-data + fi + $SCP "$CONFIG_REPO_PATH"/meta-data "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/redeploy/ + + if [ ! -r "$CONFIG_REPO_PATH"/redeploy.cfg ]; then + create_redeploy_cfg "$CONFIG_REPO_PATH"/redeploy.cfg + fi + $SCP "$CONFIG_REPO_PATH"/redeploy.cfg "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/grub/redeploy.cfg + + # ssh configs are expected to be deployed as a directory + mkdir -p "$CONFIG_REPO_PATH"/ssh/sshd_config.d + + create_sshd_conf "$CONFIG_REPO_PATH"/ssh/sshd_config.d/pc_sanity.conf + cp "$CONFIG_REPO_PATH"/authorized_keys "$CONFIG_REPO_PATH"/ssh || true # optional file + $SCP -r "$CONFIG_REPO_PATH"/ssh "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/ssh-config + + rm -rf "$CONFIG_REPO_PATH"/ssh + else + # configs follow launchapd repo folder structure + $SCP "$CONFIG_REPO_PATH"/alloem-init/cloud-configs/redeploy/meta-data "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/redeploy/ + $SCP "$CONFIG_REPO_PATH"/alloem-init/cloud-configs/redeploy/user-data "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/redeploy/ + $SCP "$CONFIG_REPO_PATH"/alloem-init/cloud-configs/grub/redeploy.cfg "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/cloud-configs/grub/redeploy.cfg + + # Copy ssh key from alloem-init injections to the target + $SCP -r "$CONFIG_REPO_PATH"/injections/alloem-init/chroot/minimal.standard.live.hotfix.squashfs/etc/ssh "$TARGET_USER"@"$addr":/home/"$TARGET_USER"/redeploy/ssh-config + fi + + # Umount the partitions + MOUNT=$($SSH "$TARGET_USER"@"$addr" -- lsblk -n -o MOUNTPOINT "$RESET_PART") + if [ -n "$MOUNT" ]; then + $SSH "$TARGET_USER"@"$addr" -- sudo umount "$RESET_PART" + fi + MOUNT=$($SSH "$TARGET_USER"@"$addr" -- lsblk -n -o MOUNTPOINT "$EFI_PART") + if [ -n "$MOUNT" ]; then + $SSH "$TARGET_USER"@"$addr" -- sudo umount "$EFI_PART" + fi + + # Format partitions + $SSH "$TARGET_USER"@"$addr" -- sudo mkfs.vfat "$RESET_PART" + $SSH "$TARGET_USER"@"$addr" -- sudo mkfs.vfat "$EFI_PART" + + # Mount ISO and reset partition + $SSH "$TARGET_USER"@"$addr" -- mkdir -p /home/"$TARGET_USER"/iso || true + $SSH "$TARGET_USER"@"$addr" -- mkdir -p /home/"$TARGET_USER"/reset || true + $SSH "$TARGET_USER"@"$addr" -- sudo mount -o loop /home/"$TARGET_USER"/"$ISO" /home/"$TARGET_USER"/iso || true + $SSH "$TARGET_USER"@"$addr" -- sudo mount "$RESET_PART" /home/"$TARGET_USER"/reset || true + + # Sync ISO to the reset partition + $SSH "$TARGET_USER"@"$addr" -- sudo rsync -avP /home/"$TARGET_USER"/iso/ /home/"$TARGET_USER"/reset || true + + # Sync cloud-configs to the reset partition + $SSH "$TARGET_USER"@"$addr" -- sudo mkdir -p /home/"$TARGET_USER"/reset/cloud-configs || true + $SSH "$TARGET_USER"@"$addr" -- sudo cp -r /home/"$TARGET_USER"/redeploy/cloud-configs/redeploy/ /home/"$TARGET_USER"/reset/cloud-configs/ + $SSH "$TARGET_USER"@"$addr" -- sudo cp -r /home/"$TARGET_USER"/redeploy/ssh-config/ /home/"$TARGET_USER"/reset/ + $SSH "$TARGET_USER"@"$addr" -- sudo cp /home/"$TARGET_USER"/redeploy/cloud-configs/grub/redeploy.cfg /home/"$TARGET_USER"/reset/boot/grub/grub.cfg + $SSH "$TARGET_USER"@"$addr" -- sudo sed -i "s/RP_PARTUUID/${RESET_PARTUUID}/" /home/"$TARGET_USER"/reset/boot/grub/grub.cfg + + # Reboot the target + $SSH "$TARGET_USER"@"$addr" -- sudo reboot || true +done + +# Clear the known hosts +for addr in "${TARGET_IPS[@]}"; +do + if [ -f "$HOME/.ssh/known_hosts" ]; then + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$addr" + fi +done + +# Polling the targets +STARTED=("${TARGET_IPS[@]}") +finished=0 +startTime=$(date +%s) +while :; +do + sleep 180 + currentTime=$(date +%s) + if [[ $((currentTime - startTime)) -gt $TIMEOUT ]]; then + echo "Timeout is reached, deployment are not finished" + break + fi + + for addr in "${STARTED[@]}"; + do + if $SSH "$TARGET_USER"@"$addr" -- exit; then + STARTED=("${STARTED[@]/$addr}") + finished=$((finished + 1)) + fi + done + + if [ $finished -eq ${#TARGET_IPS[@]} ]; then + echo "Deployment is done" + break + fi +done diff --git a/device-connectors/src/testflinger_device_connectors/devices/__init__.py b/device-connectors/src/testflinger_device_connectors/devices/__init__.py index 05101d93..03c07fa1 100644 --- a/device-connectors/src/testflinger_device_connectors/devices/__init__.py +++ b/device-connectors/src/testflinger_device_connectors/devices/__init__.py @@ -41,6 +41,7 @@ "fake_connector", "hp_oemscript", "lenovo_oemscript", + "oem_autoinstall", "maas2", "multi", "muxpi", diff --git a/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/__init__.py b/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/__init__.py new file mode 100644 index 00000000..e4b0ca3e --- /dev/null +++ b/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/__init__.py @@ -0,0 +1,42 @@ +# Copyright (C) 2024 Canonical +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Device connector to provision Ubuntu OEM on systems +that support autoinstall and image-deploy.sh script""" + +import logging + +from testflinger_device_connectors.devices import ( + DefaultDevice, + RecoveryError, + catch, +) +from testflinger_device_connectors.devices.oem_autoinstall.oem_autoinstall import ( # noqa: E501 + OemAutoinstall, +) + +logger = logging.getLogger(__name__) + + +class DeviceConnector(DefaultDevice): + """Tool for provisioning baremetal with a given image.""" + + @catch(RecoveryError, 46) + def provision(self, args): + """Method called when the command is invoked.""" + device = OemAutoinstall(args.config, args.job_data) + logger.info("BEGIN provision") + logger.info("Provisioning device") + device.provision() + logger.info("END provision") diff --git a/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/oem_autoinstall.py b/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/oem_autoinstall.py new file mode 100644 index 00000000..b825ed9f --- /dev/null +++ b/device-connectors/src/testflinger_device_connectors/devices/oem_autoinstall/oem_autoinstall.py @@ -0,0 +1,242 @@ +# Copyright (C) 2024 Canonical +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Starting from Ubuntu 24.04, OEM uses autoinstall to provision +the PC platforms for all vendors. +Use this device connector for systems that support autoinstall provisioning +with image-deploy.sh script +""" + +import json +import logging +import os +from pathlib import Path +import subprocess +import yaml +import shutil +import requests +from requests.auth import HTTPBasicAuth + +from testflinger_device_connectors.devices import ( + ProvisioningError, + RecoveryError, +) + +logger = logging.getLogger(__name__) +ATTACHMENTS_DIR = "attachments" +ATTACHMENTS_PROV_DIR = Path.cwd() / ATTACHMENTS_DIR / "provision" + + +class OemAutoinstall: + """Device Connector for OEM Script.""" + + def __init__(self, config, job_data): + with open(config, encoding="utf-8") as configfile: + self.config = yaml.safe_load(configfile) + with open(job_data, encoding="utf-8") as job_json: + self.job_data = json.load(job_json) + + def provision(self): + """Provision the device""" + + # Ensure the device is online and reachable + try: + self.test_ssh_access() + except subprocess.CalledProcessError: + self.hardreset() + self.test_ssh_access() + + provision_data = self.job_data.get("provision_data", {}) + image_url = provision_data.get("url") + token_file = provision_data.get("token_file") + user_data = provision_data.get("user_data") + redeploy_cfg = provision_data.get("redeploy_cfg") + authorized_keys = provision_data.get("authorized_keys") + + if not image_url: + logger.error( + "Please provide an image 'url' in the provision_data section" + ) + raise ProvisioningError("No image url provided") + + if not user_data: + logger.error( + "Please provide user-data file in provision_data section" + ) + raise ProvisioningError("No user-data provided") + + # image-deploy.sh expects specific filename, + # so need to rename if doesn't match + user_data_path = "user-data" + self.copy_to_deploy_path(user_data, user_data_path) + + if redeploy_cfg is not None: + redeploy_cfg_path = "redeploy.cfg" + self.copy_to_deploy_path(redeploy_cfg, redeploy_cfg_path) + if authorized_keys is not None: + authorized_keys_path = "authorized_keys" + self.copy_to_deploy_path(authorized_keys, authorized_keys_path) + + try: + image_file = self.download_with_credentials( + image_url, ATTACHMENTS_PROV_DIR / token_file + ) + self.run_deploy_script(image_file) + finally: + if image_file: + os.unlink(image_file) + + def copy_to_deploy_path(self, source_path, dest_path): + """ + Verify if attachment exists, then copy when + it's missing in deployment dir + """ + source_path = ATTACHMENTS_PROV_DIR / source_path + dest_path = ATTACHMENTS_PROV_DIR / dest_path + if not source_path.exists(): + logger.error( + f"{source_path} file was not found in attachments. " + "Please check the filename." + ) + raise ProvisioningError( + f"{source_path} file was not found in attachments" + ) + + if not dest_path.exists(): + shutil.copy(source_path, dest_path) + + def run_deploy_script(self, image_file): + """Run the script to deploy ISO and config files""" + device_ip = self.config["device_ip"] + + data_path = Path(__file__).parent / "../../data/muxpi/oem_autoinstall" + logger.info("Running deployment script") + + deploy_script = data_path / "image-deploy.sh" + cmd = [ + deploy_script, + "--iso", + image_file, + "--local-config", + ATTACHMENTS_PROV_DIR, + device_ip, + ] + + proc = subprocess.run( + cmd, + timeout=60 * 60, # 1 hour - just in case + check=False, + ) + if proc.returncode: + logger.error( + "Deploy script failed with return code %s", proc.returncode + ) + raise ProvisioningError("Deploy script failed") + + def test_ssh_access(self): + """Verify SSH access available to DUT without any prompts""" + try: + test_username = self.job_data.get("test_data", {}).get( + "test_username", "ubuntu" + ) + except AttributeError: + test_username = "ubuntu" + + cmd = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "BatchMode=yes", + "-o", + "ConnectTimeout=10", + f"{test_username}@{self.config['device_ip']}", + "true", + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + logger.error("SSH connection failed: %s", result.stderr) + raise ProvisioningError("Failed SSH to DUT") + + def hardreset(self): + """ + Reboot the device. + + :raises RecoveryError: + If the command times out or anything else fails. + + .. note:: + This function runs the commands specified in 'reboot_script' + in the config yaml. + """ + for cmd in self.config["reboot_script"]: + logger.info("Running %s", cmd) + try: + subprocess.check_call(cmd.split(), timeout=120) + except subprocess.SubprocessError as exc: + raise RecoveryError("Error running reboot script!") from exc + + def download_with_credentials(self, url, token_file=None, filename=None): + """ + Download a file from a URL + If credentials file provided, then use token to auth. + + :param url: URL of the file to download. + :param token_file: Optional path to the config file + containing 'username' and 'token'. + """ + logger.info("Downloading file from %s", url) + if filename is None: + filename = os.path.basename(url) + + # Use credentials if were provided + auth = None + if token_file: + credentials = {} + with open(token_file, "r") as file: + for line in file: + key, value = line.strip().split(":", 1) + credentials[key.strip()] = value.strip() + username = credentials.get("username") + token = credentials.get("token") + + if username and token: + auth = HTTPBasicAuth(username, token) + else: + logger.error("Credentials are missing in the config file.") + return + + # Download the file + try: + response = requests.get(url, auth=auth) + + if response.status_code == 200: + with open(filename, "wb") as file: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + file.write(chunk) + return filename + else: + logger.error( + "Failed to download file: %s", response.status_code + ) + logger.error("Failed response content: %s", response.text) + + except requests.RequestException as e: + raise ProvisioningError( + f"An error occurred in image download: {e}" + ) diff --git a/docs/.wordlist.txt b/docs/.wordlist.txt index 50a70b04..2d08984c 100644 --- a/docs/.wordlist.txt +++ b/docs/.wordlist.txt @@ -1,11 +1,13 @@ addons API APIs +authorized artifact artifacts autoinstall balancer BG +cfg CharmHub CLI CM3 @@ -25,6 +27,7 @@ favicon Git GitHub Grafana +HTTPBasicAuth IAM ISOs init @@ -62,6 +65,7 @@ noprovision NVidia observability OEM +oem oemrecovery oemscript OLM diff --git a/docs/reference/device-connector-types.rst b/docs/reference/device-connector-types.rst index 9a15184f..6d2c8991 100644 --- a/docs/reference/device-connector-types.rst +++ b/docs/reference/device-connector-types.rst @@ -32,6 +32,8 @@ To specify the commands to run by the device in each test phase, set the ``testf - This device connector is used for Lenovo OEM devices running certain versions of OEM supported images that can use a recovery partition to recover not only the same image, but in some cases, other OEM image versions as well. * - ``hp_oemscript`` - This device connector is used for HP OEM devices running certain versions of OEM supported images that can use a recovery partition to recover not only the same image, but in some cases, other OEM image versions as well. + * - ``oem_autoinstall`` + - This device connector is used for OEM PC platforms starting from Ubuntu 24.04. It executes image-deploy.sh script and consumes autoinstall configuration files to complete the installation. * - ``zapper_iot`` - This device connector is used for provisioning ubuntu-core to ARM IoT devices. It could be provision by set device to download mode or override seed partition and do recovery. * - ``zapper_kvm`` @@ -263,6 +265,106 @@ The ``hp_oemscript`` device connector does not support any ``provision_data`` ke the ``zstd`` tool is supported) and flashed to the device, which will be used to boot up the DUT. +.. _oem_autoinstall: + +oem_autoinstall +------------ + +The ``oem_autoinstall`` device connector supports the following ``provision_data`` keys. + +.. list-table:: Supported ``autoinstall`` keys for ``user_data`` config file + :header-rows: 1 + + * - Key + - Description + * - ``url`` + - URL to the image file which will be used to provision the device. + * - ``token_file`` + - Optional credentials file in :ref:`file attachments ` when ``url`` + requires authentication. These credentials will be used with HTTPBasicAuth + to download the image from ``url``. It must contain: + + username: $MY_USERNAME + + token: $MY_TOKEN + + * - ``user_data`` + - Required file provided with :ref:`file attachments `. + This file will be consumed by the autoinstall and cloud-init. + Sample user-data is provided in the section below. + * - ``redeploy_cfg`` + - Optional file provided with :ref:`file attachments `. + This file will override the grub.cfg in reset partition. + By default, boots the DUT from reset partition to start the provisioning. + * - ``authorized_keys`` + - Optional file provided with :ref:`file attachments `. + It will be copied to /etc/ssh/ on provisioned device and allows to import + keys in bulk when system does not have internet access for ssh-import-id. + The keys listed in this file are allowed to access the system in addition + to keys in ~/.ssh/authorized_keys. + +Sample cloud-config file for ``user_data`` key. It should contain directives for +autoinstall and cloud-init. Following is the basic structure example with explanations. +Optional packages, keys, users, or commands can be added to customise the installation. + +For more details, please refer to +`Autoinstall Reference `_ +on this topic + + .. code-block:: bash + + #cloud-config + # vim: syntax=yaml + + autoinstall: # autoinstall configuration for the installer (subiquity) + version: 1 + + storage: + layout: + name: direct + match: + install-media: true + + early-commands: + - "nmcli networking off" # prevents online updating packages in subiquity installer + + late-commands: + # hook.sh is a part of OEM image scripts + - "bash /cdrom/sideloads/hook.sh late-commands" + - "mount -o rw,remount /cdrom" + + # Copy /cdrom/ssh-config to /target/etc/ssh, if it exists. + # File provided in authorized_keys key is copied here. + - "! [ -d /cdrom/ssh-config ] || ( mkdir -p /target/etc/ssh && \ + cp -r /cdrom/ssh-config/* /target/etc/ssh)" + shutdown: reboot # tell the installer to reboot after installation + + # cloud-init config for the provisioned system + user-data: + bootcmd: + - "bash /sp-bootstrap/hook.sh early-welcome" + users: + - default + packages: # list of packages to be installed + - openssh-server + runcmd: + # set default ubuntu user and unlock password login + - ["usermod", "-p", "MY_PASSWORD", "ubuntu"] + - ["passwd", "-u", "ubuntu"] + + # key to be added in ~/.ssh/authorized_keys + ssh_authorized_keys: + - 'ssh-rsa MY_PUBLIC_KEY user@host' + + # Reboot after early-welcome is done + power_state: + mode: "reboot" + message: "early-welcome setup complete, rebooting..." + timeout: 30 + + bootcmd: # bootcmd of autoinstall + - ['plymouth', 'display-message', '--text', 'Starting installer...'] + .. _zapper_kvm: zapper_kvm