Add cos_xfstests_runner container in cos/tools.

BUG=b/218901513

Change-Id: I1878d5ee47228cfaa6e7834390df3b26f6504980
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/51569
Tested-by: Meena Shanmugam <meenashanmugam@google.com>
Reviewed-by: Arnav Kansal <rnv@google.com>
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
diff --git a/src/cmd/cos_xfstests_runner/Dockerfile b/src/cmd/cos_xfstests_runner/Dockerfile
new file mode 100644
index 0000000..ff0869c
--- /dev/null
+++ b/src/cmd/cos_xfstests_runner/Dockerfile
@@ -0,0 +1,10 @@
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y make git
+RUN apt-get install -y apt-transport-https ca-certificates gnupg curl sudo python3
+RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
+RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
+RUN apt-get update && apt-get install -y google-cloud-cli
+COPY ./run_config.sh /run_config.sh
+
+ENTRYPOINT ["/run_config.sh"]
diff --git a/src/cmd/cos_xfstests_runner/README.md b/src/cmd/cos_xfstests_runner/README.md
new file mode 100644
index 0000000..99ca1d7
--- /dev/null
+++ b/src/cmd/cos_xfstests_runner/README.md
@@ -0,0 +1,56 @@
+# COS Xfstests Runner container used in COS xfstests
+
+## Overview
+
+This is an docker image used by COS (Container-Optimized OS)
+xfstests. It includes scripts and necessary dependencies for running
+gce-xfstests under a given xfstests config against a given COS version.
+
+The image is released at `gcr.io/cos-xfstests/cos-xfstests-runner` with a unique
+tag for each release. This container image is not publicly available.
+
+## Building COS Xfstests Runner Image
+
+### Locally (for testing the image)
+
+For testing, you can simply build and test this docker container locally on your
+workstation:
+
+```shell
+  $ docker build -t cos-xfstests-runner:dev .
+```
+
+### Production (push into GCR)
+Since `gcr.io/cos-xfstests/cos-xfstests-runner` is not publicly available, only
+authenticated users can push into the GCR registry.
+
+```shell
+  $ VERSION=<version>  # e.g., 20171008
+  $ docker build -t gcr.io/cos-xfstests/cos-xfstests-runner:$VERSION .
+  $ docker build -t gcr.io/cos-xfstests/cos-xfstests-runner:latest .
+  $ gcloud docker -- push gcr.io/cos-xfstests/cos-xfstests-runner:$VERSION
+  $ gcloud docker -- push gcr.io/cos-xfstests/cos-xfstests-runner:latest
+```
+
+## Using COS Xfstests Runner Image
+
+### Locally (for testing the image)
+
+COS xfstests runner container will use the xfstests-bld tarball and kernel image
+generated by COS kernel CI. To run this container locally, you can build the kernel
+image using cos_kerenl_devenv container.
+
+To run the COS xfstests runner locally:
+
+```shell
+  $ docker run --rm -v ~/.config/gcloud:/root/.config/gcloud cos-xfstests-runner:dev -x overlay -n xfs-vm -r gs://xueweiz-play/xfstests/R76-12188.0.0 -p cos-xfstests -z us-west1-c -b generic/269,generic/347,generic/405,generic/500
+```
+
+The container will download xfstests-bld tarball and kernel image from given GCS
+bucket, then run `gce-xfstests` script on the given config. This will create one
+GCE instance and two responding disks in the given project and zone. If they run
+into soft lockup, the developer must manually clean them up.
+
+The container image is set to use
+[test appliance image](https://github.com/tytso/xfstests-bld/blob/master/Documentation/gce-xfstests.md#creating-a-new-gce-test-appliance-image)
+"xfstests-201901211635" by default. Use the `--rootfs_image` to overwrite it.
diff --git a/src/cmd/cos_xfstests_runner/cloudbuild.yaml b/src/cmd/cos_xfstests_runner/cloudbuild.yaml
new file mode 100644
index 0000000..dcb05c9
--- /dev/null
+++ b/src/cmd/cos_xfstests_runner/cloudbuild.yaml
@@ -0,0 +1,24 @@
+options:
+  env:
+  - 'DOCKER_CLI_EXPERIMENTAL=enabled'
+steps:
+# This step is needed to add a new entry to /proc/sys/fs/binfmt_misc. Docker
+# uses QEMU user emulation to run arm64 programs on x86 hosts. A QEMU
+# interpreter needs to be added to /proc/sys/fs/binfmt_misc to run arm64
+# programs.
+- name: 'gcr.io/cloud-builders/docker'
+  args: ['run', '--privileged', 'linuxkit/binfmt:a17941b47f5cb262638cfb49ffc59ac5ac2bf334-amd64']
+# The default builder (which appears to be the Docker daemon that implements
+# the old, familiar `docker build` behavior) doesn't support the --platform
+# flag, so we need to create a new builder.
+- name: 'gcr.io/cloud-builders/docker'
+  args: ['buildx', 'create', '--name', 'builder']
+- name: 'gcr.io/cloud-builders/docker'
+  args: ['buildx', 'use', 'builder']
+# Images produced in this way do not appear in the Docker image registry shown
+# by `docker images`, at least by default. We use the --push flag to push the
+# image after building it, because a subsequent `docker push` won't find the
+# image locally.
+- name: 'gcr.io/cloud-builders/docker'
+  args: ['buildx', 'build', '--platform', 'linux/amd64,linux/arm64', '-f', 'src/cmd/cos_xfstests_runner/Dockerfile', '-t', 'gcr.io/${_OUTPUT_PROJECT}/cos_xfstests_runner:latest', '-t', 'gcr.io/${_OUTPUT_PROJECT}/cos_xfstests_runner:${TAG_NAME}', '--push', 'src/cmd/cos_xfstests_runner']
+timeout: 1800s
diff --git a/src/cmd/cos_xfstests_runner/run_config.sh b/src/cmd/cos_xfstests_runner/run_config.sh
new file mode 100755
index 0000000..7db972a
--- /dev/null
+++ b/src/cmd/cos_xfstests_runner/run_config.sh
@@ -0,0 +1,172 @@
+#!/bin/bash
+# Run xfstests for a given file system configuration.
+
+set -eu
+set -o pipefail
+
+readonly PROG_NAME=$(basename "$0")
+readonly XFSTESTS_BLD_DIR="/home/fstests"
+readonly XFSTESTS_CFG_DIR="test-appliance/files/root/fs/"
+
+CONFIG=""
+INSTANCE_NAME=""
+RESULTS_BUCKET=""
+PROJECT=""
+ZONE=""
+GROUP="auto"
+XFSTESTS_EXCLUDED=""
+ARCH="x86_64"
+
+#
+# usage <exit_code>
+#
+# Print usage and exit.
+#
+usage() {
+        local exit_code="${1}"
+
+        cat <<EOF
+Usage:
+        ${PROG_NAME} [-x <xfstests_config>] [-n <instance_name>]
+        -x, --xfstests_config   the configuration to run xfstests on
+        -n, --instance_name     the name of the instance running xfstests
+        -r, --result_bucket     the GCS bucket to store test result
+        -p, --project           the GCP project to run the test VM
+        -z, --zone              the GCE zone to run the test VM
+        -g, --group             the xfstest group to run
+        -b, --blocked_tests     the list of tests to not run
+        -i, --rootfs_image      the test appliance image to use
+        -a, --arch              the architecture of the image
+
+Note that this script expect kernel to be located at [result_bucket]/bzImage.
+
+Examples:
+        $ ${PROG_NAME} -x overlay -n xfs-vm -r gs://xfstests/R93-11647.62.0 -p cos-xfstests -z us-west1-c -b generic/269,generic/500
+
+EOF
+        exit "${exit_code}"
+}
+
+#
+# parse_args <args...>
+#
+# Parse command line arguments.
+#
+parse_args() {
+  local args
+
+  args=$(getopt \
+          --options "x:n:r:p:z:g:b:a:" \
+          --longoptions "xfstests_config: instance_name: result_bucket: project: zone: group: blocked_tests: arch:" \
+          -- "$@")
+  [[ $? -eq 0 ]] || usage 1
+  eval set -- "${args}"
+
+  while :; do
+    arg="${1}"
+    shift
+    case "${arg}" in
+    -x|--xfstests_config) CONFIG="${1}"; shift ;;
+    -n|--instance_name) INSTANCE_NAME="${1}"; shift ;;
+    -r|--result_bucket) RESULTS_BUCKET="${1}"; shift ;;
+    -p|--project) PROJECT="${1}"; shift ;;
+    -z|--zone) ZONE="${1}"; shift ;;
+    -g|--group) GROUP="${1}"; shift ;;
+    -b|--blocked_tests) XFSTESTS_EXCLUDED="${1}"; shift ;;
+    -i|--rootfs_image) TEST_APPLIANCE_IMAGE="${1}"; shift ;;
+    -a|--arch) ARCH="${1}"; shift ;;
+    --) break ;;
+    *) echo "internal error parsing arguments!"; usage 1 ;;
+    esac
+  done
+
+  if [[ -z "$CONFIG" || -z "$RESULTS_BUCKET" || -z "$PROJECT" || -z "$ZONE" ]] ; then
+    usage 1
+  fi
+
+  if [[ -z "$INSTANCE_NAME" ]] ; then
+    echo "Instance name missing. Randomly generating one."
+    CONFIG_ALNUM=${CONFIG//[^[:alnum:]]/}
+    INSTANCE_NAME="cos-xfstests-$(date +"%Y%m%d%H%M")${RANDOM}-${CONFIG_ALNUM}"
+    echo "Instance name: $INSTANCE_NAME"
+  else
+    echo "Given instance name: $INSTANCE_NAME"
+  fi
+  INSTANCE_NAME=$(echo "${INSTANCE_NAME}" | sed 's/_/-/g')
+  echo "Using instance name: $INSTANCE_NAME"
+}
+
+setup_xfstests_bld() {
+    git clone https://github.com/tytso/xfstests-bld $XFSTESTS_BLD_DIR
+    cd "$XFSTESTS_BLD_DIR" && make
+}
+
+#
+# gce_xfstests_run
+#
+# Run the gce_xfstests command
+#
+gce_xfstests_run() {
+  # The result bucket name without gs:// prefix.
+  local bucket_name="${RESULTS_BUCKET#gs://}"
+  if [[ "${ARCH}" = "x86_64" ]]; then
+    kernel="${RESULTS_BUCKET}/bzImage"
+  elif [[ "${ARCH}" = "arm64" ]]; then
+    kernel="${RESULTS_BUCKET}/Image"
+  else
+    echo "Unknown architecture: ${ARCH}"
+    exit 1
+  fi
+
+  # Sets up the config file for gce-xfstests.
+  mkdir -p /root/.config
+  cat <<EOF > /root/.config/gce-xfstests
+GS_BUCKET=${bucket_name}
+GCE_PROJECT=${PROJECT}
+GCE_IMAGE_PROJECT=${PROJECT}
+GCE_ZONE=${ZONE}
+GCE_KERNEL=${kernel}
+GCE_UPLOAD_SUMMARY=true
+EOF
+
+  local excluded_arg=""
+  local excluded_val=""
+  if [[ ! -z "${XFSTESTS_EXCLUDED}" ]] ; then
+    # In COS xfsquick test suite has many excluded test cases(~120 test cases),
+    # which are passed as command line arguments to gce-xfstests script.
+    # The arguments that we are passing to gce-xfstests is passed as kernel
+    # command line to kexec(This is added recently ). Since cos xfs is having
+    # too many excluded test cases, the kernel command line becomes too big and
+    # kexec fails with "Kernel command line too long for kernel! Cannot load /root/bzImage"
+    # Hence adding the excluded tests in a file instead of passing as an argument.
+
+    if [[ "${CONFIG}" = "xfs" ]] ; then
+      echo "${XFSTESTS_EXCLUDED}" | sed 's/,/\n/g' >> \
+           "${XFSTESTS_BLD_DIR}"/"${XFSTESTS_CFG_DIR}"/"${CONFIG}"/exclude
+      excluded_arg="--update-files"
+    else
+      excluded_arg="-X"
+      excluded_val="${XFSTESTS_EXCLUDED}"
+
+    fi
+  fi
+  echo $XFSTESTS_BLD_DIR/gce-xfstests --instance-name "${INSTANCE_NAME}" \
+    --kernel "${RESULTS_BUCKET}/bzImage" ${excluded_arg} "${excluded_val}" \
+    -c "${CONFIG}" -g "${GROUP}" --arch "${ARCH}"
+  # shellcheck disable=SC2086
+  # putting ${excluded_val} in quotes causes gce-xfstests to fail because it
+  # it takes empty value as an argument.
+  $XFSTESTS_BLD_DIR/gce-xfstests --instance-name "${INSTANCE_NAME}" \
+    --kernel "${kernel}" ${excluded_arg} ${excluded_val} \
+    -c "${CONFIG}" -g "${GROUP}" --arch "${ARCH}"
+}
+
+main() {
+  parse_args "$@"
+
+  setup_xfstests_bld
+
+  gce_xfstests_run
+}
+
+main "$@"