qemu: add linux boot tests

Add some basic boot tests for QEMU and supporting scripts to automate
the generation of dependencies. Currently, there are no tests in CI that
utilise the platform. This makes it easier to do testing in CI without
the need for a complex hardware setup.

Change-Id: I98e43ecc56dc44a767590c73963ded7b2cee1cff
Signed-off-by: Harrison Mutai <harrison.mutai@arm.com>
diff --git a/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt b/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt
diff --git a/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt-debug b/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt-debug
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/group/qemu-boot-tests/qemu-default:qemu-linux.rootfs-fip.uefi-virt-debug
diff --git a/model/qemu-virt.sh b/model/qemu-virt.sh
new file mode 100644
index 0000000..e8070fc
--- /dev/null
+++ b/model/qemu-virt.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2022 Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+# Generates a machine configuration for QEMU virt.
+set_model_path qemu-system-aarch64
+
+cat <<EOF >"$model_param_file"
+-M virt
+-machine 'secure=on,virtualization=on,gic-version=2'
+-cpu max
+-smp 4
+-m 4G
+-nographic -display none -d unimp
+-append 'console=ttyAMA0,115200n8 root=/dev/vda earlycon'
+${kernel_bin+-kernel $kernel_bin}
+${rootfs_bin+-initrd $rootfs_bin}
+${qemu_bios_bin+-bios $qemu_bios_bin}
+${wait_debugger+-gdb tcp:localhost:9000}
+${wait_debugger+-S}
+EOF
diff --git a/qemu_utils.sh b/qemu_utils.sh
new file mode 100644
index 0000000..29b8e61
--- /dev/null
+++ b/qemu_utils.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2022 Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+set -u
+
+rootfs_url="$tfa_downloads/linux_boot/busybox.cpio.gz"
+uefi_url="$tfa_downloads/linux_boot/qemu/QEMU_EFI.fd"
+
+# Default QEMU model variables
+default_model_dtb="dtb.bin"
+
+# QEMU Kernel URLs
+declare -A plat_kernel_list=(
+	[qemu-busybox]="$tfa_downloads/linux_boot/Image.gz"
+)
+
+gen_qemu_yaml(){
+	model="${model:?}"
+	model_bin="${model_bin:qemu-system-aarch64}"
+
+	yaml_template_file="$workspace/qemu_template.yaml"
+	yaml_file="$workspace/qemu.yaml"
+	yaml_job_file="$workspace/job.yaml"
+	lava_model_params="$workspace/lava_model_params"
+
+	# this function expects a template, quit if it is not present
+	if [ ! -f "$yaml_template_file" ]; then
+		return
+	fi
+
+	prompt="${prompt:-root@tf-busyboot:/root#}"
+
+	# Any addition on this array requires an addition in the qemu
+	# templates.
+	declare -A qemu_artefact_urls=(
+		[kernel]="$(gen_bin_url kernel.bin)"
+		[bios]="$(gen_bin_url qemu_bios.bin)"
+		[initrd]="$(gen_bin_url rootfs.bin.gz)"
+		[uboot]="$(gen_bin_url uboot.bin)"
+	)
+
+	declare -A qemu_artefact_filters=(
+		[kernel]="kernel.bin"
+		[bios]="qemu_bios.bin"
+		[initrd]="rootfs.bin"
+		[uboot]="uboot.bin"
+	)
+
+	declare -A qemu_artefact_macros=(
+		["kernel.bin"]="{kernel}"
+		["qemu_bios.bin"]="{bios}"
+		["rootfs.bin"]="{initrd}"
+		["uboot.bin"]="{uboot}"
+	)
+
+	declare -a qemu_artefacts
+	filter_artefacts qemu_artefacts qemu_artefact_filters
+
+	lava_model_params="${lava_model_params}" \
+		gen_lava_model_params qemu_artefact_macros
+
+	yaml_template_file="$yaml_template_file" \
+	yaml_file="$yaml_file" \
+	yaml_job_file="$yaml_job_file" \
+		gen_lava_job_def qemu_artefacts qemu_artefact_urls
+}
+
+gen_qemu_image(){
+	local image=${image:?}
+	local bl1_path=${bl1_path:?}
+	local fip_path=${fip_path:?}
+
+	# Cocatenate bl1 and fip images to create a single BIOS consumed by QEMU.
+	cp $bl1_path "$image"
+	dd if=$fip_path of="$image" bs=64k seek=4
+
+	archive_file "$image"
+}
+
+set +u
diff --git a/run_config/qemu-fip.uefi b/run_config/qemu-fip.uefi
new file mode 100644
index 0000000..0b58894
--- /dev/null
+++ b/run_config/qemu-fip.uefi
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2022 Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+post_tf_build(){
+	url="$uefi_url" filename="uefi.bin" fetch_and_archive
+
+	build_fip BL33="$archive/uefi.bin"
+}
+
+post_tf_archive(){
+	image="qemu_bios.bin" bl1_path="$archive/bl1.bin" \
+		fip_path="$archive/fip.bin" gen_qemu_image
+}
diff --git a/run_config/qemu-linux.rootfs b/run_config/qemu-linux.rootfs
new file mode 100644
index 0000000..fe5b4d1
--- /dev/null
+++ b/run_config/qemu-linux.rootfs
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2022 Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+fetch_tf_resource() {
+	kernel_type="qemu-busybox" get_kernel
+	url="$rootfs_url" filename="rootfs.bin" fetch_and_archive
+}
diff --git a/run_config/qemu-virt b/run_config/qemu-virt
new file mode 100644
index 0000000..0e6e114
--- /dev/null
+++ b/run_config/qemu-virt
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2022 Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+generate_lava_job_template(){
+	payload_type="linux" target="qemu" gen_yaml_template
+}
+
+generate_lava_job(){
+	local model="qemu-virt"
+
+	model="$model" gen_model_params
+	model="$model" gen_qemu_yaml
+}
diff --git a/script/lava-templates/qemu-linux.yaml b/script/lava-templates/qemu-linux.yaml
new file mode 100644
index 0000000..814b20e
--- /dev/null
+++ b/script/lava-templates/qemu-linux.yaml
@@ -0,0 +1,85 @@
+device_type: qemu
+job_name: qemu-linux-${test_config}
+priority: medium
+visibility: public
+
+metadata:
+  test_config: ${test_config}
+  build_url: ${BUILD_URL}
+  emulator: ${model}
+
+$(if [ -n "${gerrit_url}" ]; then
+	cat <<-YAML
+  gerrit_url: "${gerrit_url}"
+	YAML
+fi)
+
+context:
+  arch: aarch64
+  machine: ${machine:-virt}
+  cpu: ${cpu:-max}
+  extra_options:
+$(for boot_argument in "${boot_arguments[@]:?}"; do
+	cat <<-YAML
+	    - ${boot_argument}
+	YAML
+done)
+
+timeouts:
+  job:
+    minutes: 15
+  actions:
+      login-action:
+        minutes: 5
+      auto-login-action:
+        minutes: 2
+      boot-image-retry:
+        minutes: 2
+      boot-qemu-image:
+        minutes: 2
+      bootloader-action:
+        minutes: 3
+      bootloader-commands:
+        minutes: 3
+      bootloader-interrupt:
+        seconds: 30
+      bootloader-retry:
+        minutes: 3
+      download-retry:
+        minutes: 5
+      lava-test-shell:
+        minutes: 3
+      nfs-deploy:
+        minutes: 10
+      power-off:
+        seconds: 10
+      reset-device:
+        seconds: 30
+  connection:
+    minutes: 2
+
+actions:
+- deploy:
+    to: tmpfs
+    images:
+$(for artefact in "${artefacts[@]:?}"; do
+	cat <<-YAML
+      ${artefact:?}:
+        url: ${artefact_urls[${artefact}]:?}
+	YAML
+
+	[[ "${artefact:?}" =~ ^(kernel|bios|initrd)$ ]] && cat <<-YAML
+        image_arg: -${artefact} {${artefact}}
+	YAML
+
+	[[ "${artefact:?}" =~ ^(busybox|initrd)$ ]] && cat <<-YAML
+        compression: gz
+	YAML
+done)
+
+- boot:
+    method: qemu
+    media: tmpfs
+    prompts: ["${prompt}"]
+    timeout:
+      minutes: 2
diff --git a/script/run_local_ci.sh b/script/run_local_ci.sh
index fd71153..a909c14 100755
--- a/script/run_local_ci.sh
+++ b/script/run_local_ci.sh
@@ -130,8 +130,8 @@
 			;;
 
 		"run")
-			# Local runs for FVP or arm_fpga unless asked not to
-			if echo "$RUN_CONFIG" | grep -q "^fvp" && \
+			# Local runs for FVP, QEMU, or arm_fpga unless asked not to
+			if echo "$RUN_CONFIG" | grep -q "^\(fvp\|qemu\)" && \
 					not_upon "$skip_runs"; then
 				echo "running: $config_string" >&5
 				if [ -n "$cc_enable" ]; then
diff --git a/utils.sh b/utils.sh
index 4716c804..0f7d82a 100644
--- a/utils.sh
+++ b/utils.sh
@@ -145,6 +145,119 @@
 	fi
 }
 
+fetch_and_archive() {
+	url=${url:?}
+	filename=${filename:-basename $url}
+
+	url="$url" saveas="$filename" fetch_file
+	archive_file "$filename"
+}
+
+filter_artefacts(){
+	local model_param_file="${model_param_file-$archive/model_params}"
+
+	# Bash doesn't have array values, we have to create references to the
+	# array of artefacts and the artefact filters.
+	declare -ga "$1"
+	declare -n artefacts="$1"
+	declare -n filters="$2"
+
+	for artefact in "${!filters[@]}"; do
+		if grep -E -q "${filters[${artefact}]}" "$model_param_file"; then
+			artefacts+=("${artefact}")
+		fi
+	done
+}
+
+gen_lava_job_def() {
+	local yaml_template_file="${yaml_template_file:?}"
+	local yaml_file="${yaml_file:?}"
+	local yaml_job_file="${yaml_job_file}"
+
+	# Bash doesn't have array values, we have to create references to the
+	# array of artefacts and their urls.
+	declare -n artefacts="$1"
+	declare -n artefact_urls="$2"
+
+	readarray -t boot_arguments < "${lava_model_params}"
+
+	# Generate the LAVA job definition, minus the test expectations
+	expand_template "${yaml_template_file}" > "${yaml_file}"
+
+	if [[ ! $model =~ "qemu" ]]; then
+		# Append expect commands into the job definition through
+		# test-interactive commands
+		gen_fvp_yaml_expect >> "$yaml_file"
+	fi
+
+	# create job.yaml
+	cp "$yaml_file" "$yaml_job_file"
+
+	# archive both yamls
+	archive_file "$yaml_file"
+	archive_file "$yaml_job_file"
+}
+
+gen_lava_model_params() {
+	local lava_model_params="${lava_model_params:?}"
+	declare -n macros="$1"
+
+	# Derive LAVA model parameters from the non-LAVA ones
+	cp "${archive}/model_params" "${lava_model_params}"
+
+	if [[ $model =~ "qemu" ]]; then
+		# Strip the model parameters of parameters already specified in the deploy
+		# overlay and job context.
+		sed -i '/-M/d;/kernel/d;/initrd/d;/bios/d;/cpu/d;/^[[:space:]]*$/d' \
+				$lava_model_params
+	elif [[ ! $model =~ "qemu" ]]; then
+		# FIXME find a way to properly match FVP configurations.
+		# Ensure braces in the FVP model parameters are not accidentally
+		# interpreted as LAVA macros.
+		sed -i -e 's/{/{{/g' "${lava_model_params}"
+		sed -i -e 's/}/}}/g' "${lava_model_params}"
+	else
+		echo "Unsupported emulated platform $model."
+	fi
+
+	# LAVA expects binary paths as macros, i.e. `{X}` instead of `x.bin`, so
+	# replace the file paths in our pre-generated model parameters.
+	for regex in "${!macros[@]}"; do
+		sed -i -e "s!${regex}!${macros[${regex}]}!" "${lava_model_params}"
+	done
+}
+
+gen_yaml_template() {
+	local target="${target-fvp}"
+	local yaml_template_file="${yaml_template_file-$workspace/${target}_template.yaml}"
+
+	local payload_type="${payload_type:?}"
+
+	cp "${ci_root}/script/lava-templates/${target}-${payload_type:?}.yaml" \
+		"${yaml_template_file}"
+
+	archive_file "$yaml_template_file"
+}
+
+# Generate link to an archived binary.
+gen_bin_url() {
+	local bin_mode="${bin_mode:?}"
+	local bin="${1:?}"
+
+	if upon "$jenkins_run"; then
+		echo "$jenkins_url/job/$JOB_NAME/$BUILD_NUMBER/artifact/artefacts/$bin_mode/$bin"
+	else
+		echo "file://$workspace/artefacts/$bin_mode/$bin"
+	fi
+}
+
+get_kernel() {
+	local kernel_type="${kernel_type:?}"
+	local url="${plat_kernel_list[$kernel_type]}"
+
+	url="${url:?}" filename="kernel.bin" fetch_and_archive
+}
+
 # Make a temporary directory/file insdie workspace, so that it doesn't need to
 # be cleaned up. Jenkins is setup to clean up workspace before a job runs.
 mktempdir() {