diff --git a/.github/workflows/handle_guest_network_ports.py b/.github/workflows/handle_guest_network_ports.py new file mode 100644 index 00000000..9128be38 --- /dev/null +++ b/.github/workflows/handle_guest_network_ports.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: Apache-2.0 + +''' +Tool to handle the allocation of guest network ports to multiple SNP guest without network port conflict, and +to cleanup the inactive guest network port in the GH Action Workflow Guest n/w Port inventory file + +Pre-requisite for this tool use: + Set DOTENV_PATH(environment variable) on the host with the .env file path having GHAW_GUEST_PORT_FILE +''' + +import subprocess +import argparse +from dotenv import load_dotenv +import os + +# Gets GHAW guest port file location +dotenv_path = os.path.join(os.path.dirname(__file__), os.getenv("DOTENV_PATH")) +load_dotenv(dotenv_path) + +ghaw_taken_ports_file = os.getenv("GHAW_GUEST_PORT_FILE") +if not ghaw_taken_ports_file: + print("Set DOTENV_PATH(environment variable) on host with the .env file path having GHAW_GUEST_PORT_FILE!") + exit() + +def execute_bash_command(command): + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout.strip() + +def read_ports_from_file(filename): + ports_in_use = [] + with open(filename, 'r') as file: + for line in file: + ports_in_use.append(line.strip()) + return ports_in_use + +def get_next_available_port(starting_port, ending_port): + ''' + Returns the unused network port from the network port range to run multiple SNP guest without port conflicts + ''' + + # Reads the guest port in use by GH Action workflow + ghaw_taken_ports = read_ports_from_file(ghaw_taken_ports_file) + ghaw_taken_ports = list(map(int, ghaw_taken_ports)) + + # Assumption: All n/w ports are used up + all_ports_used=1 + port_to_use=-1 + + for port_number in range(starting_port, ending_port+1): + port_status=f"sudo netstat -plnt | grep ':{port_number}'" + running_guest_port= execute_bash_command(port_status) + + # Assigns unused n/w port number + if not running_guest_port and port_number not in ghaw_taken_ports: + port_to_use=port_number + all_ports_used=0 + + # Notes unused n/w port to avoid port conflicts in GHAW + with open(ghaw_taken_ports_file, "a") as file: + file.write(str(port_to_use)+"\n") + break + + if all_ports_used == 0: + print(port_number) + else: + print("No network port is available!") + print("\n All ports in a given network range are taken up!") + +def remove_ghaw_used_ports(ghaw_port_number): + ''' + Removes the used guest port after SNP Guest test is completed for the cleanup GHAW process + ''' + try: + with open(ghaw_taken_ports_file, 'r') as fr: + lines = fr.readlines() + flag_ghaw_port_number=0 + with open(ghaw_taken_ports_file, 'w') as fw: + for line in lines: + if line.strip('\n') != str(ghaw_port_number): + fw.write(line) + else: + flag_ghaw_port_number=1 + + if flag_ghaw_port_number == 1: + print(f"Guest network port {ghaw_port_number} is removed from GH Action Workflow use!") + else: + print(f"Guest network port {ghaw_port_number} is not in use by GH Action Workflow!") + except: + print("GH Action guest ports inventory file not found on the host!") + +def main(): + parser = argparse.ArgumentParser(description='Tool to handle SNP Guest network port allocation for the network port range') + subparsers = parser.add_subparsers(dest='command') + + # Command 1: Allocates unused port number between the network port range for the SNP guest port allocation + parser_1 = subparsers.add_parser('get-next-available-port-number', help='Get the next available port to use for the given network port range') + parser_1.add_argument('--starting_port', type=int, help='Starting port number of the network port range', default=49152) + parser_1.add_argument('--ending_port', type=int, help='Ending port number fof the network port range', default=65535) + parser_1.set_defaults(func=get_next_available_port) + + # Command 2: Removes used guest port as a GH Action SNP guest cleanup process + parser_2 = subparsers.add_parser('remove-ghaw-used-port-number', help='Remove the ports in use by GH action workflow') + parser_2.add_argument('ghaw_port_number', type=int, help='Port number in use by GH Action workflow') + parser_2.set_defaults(func=remove_ghaw_used_ports) + + args = parser.parse_args() + + if args.command == 'get-next-available-port-number': + get_next_available_port(args.starting_port, args.ending_port) + elif args.command == 'remove-ghaw-used-port-number': + remove_ghaw_used_ports(args.ghaw_port_number) + else: + parser.print_help() + +if __name__ == '__main__': + main() + diff --git a/.github/workflows/sev_ci_pr_test.yaml b/.github/workflows/sev_ci_pr_test.yaml new file mode 100644 index 00000000..ff72f4fc --- /dev/null +++ b/.github/workflows/sev_ci_pr_test.yaml @@ -0,0 +1,207 @@ +name: SEV CI PR test + +on: + pull_request_target: + types: + - reopened + - opened + - edited + - synchronize + workflow_dispatch: + inputs: + pull_request_number: + description: 'Specify the pull request number' + required: true + pull_request_branch: + description: 'Specify the pull request source branch' + required: true + +jobs: + host_firmware_tests: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Show the active SNP host kernel version on the host + run: uname -r + + - name: Check if SNP is enabled on the host + run: | + set -e + source ./.github/workflows/snp_function_declarations.sh + verify_snp_host + + - name: Set the PR number and PR branch environment based on GH Action event type + run: | + event_pr_number='' + event_pr_branch='' + + if [ ${{ github.event_name }} == "pull_request_target" ]; then + event_pr_number=${{ github.event.pull_request.number }} + event_pr_branch=${{ github.event.pull_request.head.ref }} + elif [ ${{ github.event_name }} == "workflow_dispatch" ]; then + event_pr_number=${{ github.event.inputs.pull_request_number }} + event_pr_branch=${{ github.event.inputs.pull_request_branch }} + fi + + echo "pr_number=${event_pr_number}" >> $GITHUB_ENV + echo "pr_branch=${event_pr_branch}" >> $GITHUB_ENV + + - name: Show the GH environment variable current values + run: | + echo "GH Action PR number = ${{ env.pr_number }}" + echo "GH Action PR branch = ${{ env.pr_branch }}" + + - name: Run sev library cargo test on the host(without flags) + run: | + set -e + + # Give user access to /dev/sev to run cargo tests w/o permission issues + sudo usermod -a -G kvm $USER + sudo setfacl -m g:kvm:rw /dev/sev + + # Install dependencies on the host + source ./.github/workflows/snp_function_declarations.sh + check_rust_on_host + + # Fetch and checkout SEV PR on the host + cd ${HOME} + git clone https://github.com/virtee/sev.git + cd sev + + # Checkout the PR branch + if [[ ${{ github.event_name }} == "pull_request_target" || ${{ github.event_name }} == "workflow_dispatch" ]]; then + git fetch origin pull/${{ env.pr_number }}/head:${{ env.pr_branch }} + git switch ${{ env.pr_branch }} + fi + + # Cargo SEV PR test on the host + cargo test + + - name: Cleanup sev on the host + if: success() || failure() + run: rm -rf ${HOME}/sev + + snp_guest_tests: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Sleep for 35 seconds + run: sleep 35 + + - name: Set the next available guest network port number + run: | + export DOTENV_PATH="${HOME}/.env" + echo "guest_port_in_use=$(python ./.github/workflows/handle_guest_network_ports.py get-next-available-port-number)" >> $GITHUB_ENV + + - name: Set the PR number and PR branch environment based on GH Action event type + run: | + event_pr_number='' + event_pr_branch='' + + if [ ${{ github.event_name }} == "pull_request_target" ]; then + event_pr_number=${{ github.event.pull_request.number }} + event_pr_branch=${{ github.event.pull_request.head.ref }} + elif [ ${{ github.event_name }} == "workflow_dispatch" ]; then + echo "workflow dispatch" + event_pr_number=${{ github.event.inputs.pull_request_number }} + event_pr_branch=${{ github.event.inputs.pull_request_branch }} + fi + + echo "pr_number=${event_pr_number}" >> $GITHUB_ENV + echo "pr_branch=${event_pr_branch}" >> $GITHUB_ENV + + - name: View and set the SNP guest name + run: | + echo "Guest Name = snp-guest-sev-${{ env.pr_number }}" + echo "guest_name=snp-guest-sev-${{ env.pr_number }}" >> $GITHUB_ENV + + - name: Show the GH environment variable current values + run: | + echo "current guest port in use = ${{ env.guest_port_in_use }}" + echo "GH Action PR number = ${{ env.pr_number }}" + echo "GH Action PR branch = ${{ env.pr_branch }}" + + - name: Launch SNP enabled guest + run: | + set -e + wget https://raw.githubusercontent.com/LakshmiSaiHarika/sev-utils/Fedora-Latest-SNP-kernel-Upstream/tools/snp.sh + chmod +x snp.sh + + export GUEST_NAME=${{ env.guest_name }} + export HOST_SSH_PORT=${{ env.guest_port_in_use }} + + ./snp.sh launch-guest + + - name: Show SNP enabled guest qemu commandline in use + run: cat ${HOME}/snp/launch/${{ env.guest_name }}/qemu.cmdline + + - name: Show the SNP Guest Kernel version + run: | + set -e + + source ./.github/workflows/snp_function_declarations.sh + ssh_guest_command "uname -r" ${{ env.guest_name }} ${{ env.guest_port_in_use }} + + - name: Verify SNP on the guest via MSR + run: | + set -e + + source ./.github/workflows/snp_function_declarations.sh + verify_snp_guest_msr ${{ env.guest_name }} ${{ env.guest_port_in_use }} + + - name: Run sev library cargo test on the guest(without flags) + run: | + set -e + source ./.github/workflows/snp_function_declarations.sh + + # Install sev dependencies as a root user + ssh_guest_command "sudo su - </dev/null + sudo dnf install -y git gcc + EOF" ${{ env.guest_name }} ${{ env.guest_port_in_use }} + + # Perform sev CI PR test on SNP guest as root user to fix OS permission denied issues + ssh_guest_command "sudo su - <&1 >/dev/null; then + echo -e "SEV-SNP not enabled on the host. Please follow these steps to enable:\n\ + $(echo "${AMDSEV_URL}" | sed 's|\.git$||g')/tree/${AMDSEV_DEFAULT_BRANCH}#prepare-host" + return 1 +fi +} + +check_rust_on_host() { + # Install Rust on the host + source "${HOME}/.cargo/env" 2>/dev/null || true + if ! command -v rustc &> /dev/null; then + echo "Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -sSf | sh -s -- -y + source "${HOME}/.cargo/env" 2>/dev/null + fi +} + +ssh_guest_command() { + local guest_name="$2" + local GUEST_SSH_KEY_PATH="${HOME}/snp/launch/${guest_name}/${guest_name}-key" + if [ ! -f "${GUEST_SSH_KEY_PATH}" ]; then + echo "ERROR: Guest SSH key file path not present!" + exit 1 + fi + command="$1" + guest_port_in_use="$3" + + ssh -p ${guest_port_in_use} -i "${GUEST_SSH_KEY_PATH}" -o "StrictHostKeyChecking no" -o "PasswordAuthentication=no" -o ConnectTimeout=1 amd@localhost "${command}" + } + +# verify_snp_guest_msr CLI use: verify_snp_guest_msr "${guest_name}" "${guest_port_number}" +verify_snp_guest_msr(){ + # Install guest rdmsr package dependencies to insert guest msr module + ssh_guest_command "sudo dnf install -y msr-tools > /dev/null 2>&1" $1 $2> /dev/null 2>&1 + ssh_guest_command "sudo modprobe msr" $1 $2 > /dev/null 2>&1 + local guest_msr_read=$(ssh_guest_command "sudo rdmsr -p 0 0xc0010131" $1 $2) + guest_msr_read=$(echo "${guest_msr_read}" | tr -d '\r' | bc) + + # Map all the sev features in a single associative array for all guest SEV features + declare -A actual_sev_snp_bit_status=( + [SEV]=$(( ( guest_msr_read >> 0) & 1)) + [SEV-ES]=$(( (guest_msr_read >> 1) & 1)) + [SNP]=$(( (guest_msr_read >> 2) & 1)) + ) + + local sev_snp_error="" + for sev_snp_key in "${!actual_sev_snp_bit_status[@]}"; + do + if [[ ${actual_sev_snp_bit_status[$sev_snp_key]} != 1 ]]; then + # Capture the guest SEV/SNP bit value mismatch + sev_snp_error+=$(echo "$sev_snp_key feature is not active on the guest.\n"); + fi + done + + if [[ ! -z "${sev_snp_error}" ]]; then + >&2 echo -e "ERROR: ${sev_snp_error}" + return 1 + fi + } + diff --git a/.github/workflows/snp_host_setup.yaml b/.github/workflows/snp_host_setup.yaml new file mode 100644 index 00000000..189a5378 --- /dev/null +++ b/.github/workflows/snp_host_setup.yaml @@ -0,0 +1,123 @@ +name: Setup the given SNP latest upstream kernel on the self-hosted runner + +on: + workflow_dispatch: + inputs: + snp-kernel-host-guest-tag: + description: 'Specify SNP kernel tag version(e.g: v6.10)' + +jobs: + snp_setup_host: + runs-on: self-hosted + timeout-minutes: 60 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Pre-cleanup tasks for the SNP host kernel setup + run: | + [ ! -f "${HOME}/snp.sh" ] || rm -rf ${HOME}/snp.sh + [ ! -d "${HOME}/previous_snp" ] || rm -rf ${HOME}/previous_snp + [ ! -d "${HOME}/snp" ] || mv ${HOME}/snp ${HOME}/previous_snp + + - name: Install the given SNP host Kernel version on the self-hosted runner + id: install-snp-host-kernel + run: | + # Downloads sev utility script to setup host SNP kernel + wget https://raw.githubusercontent.com/LakshmiSaiHarika/sev-utils/Fedora-Latest-SNP-kernel-Upstream/tools/snp.sh + chmod +x snp.sh + + # Sets up the user given latest upstream SNP host kernel/master branch + kernel_host_guest_branch_tag="${{ github.event.inputs.snp-kernel-host-guest-tag }}" + if [[ ! -z "${kernel_host_guest_branch_tag}" ]]; then + ./snp.sh --kernel-tag "${kernel_host_guest_branch_tag}" setup-host + else + # Uses kernel upstream default master branch for kernel installation + ./snp.sh setup-host + fi + + - name: Revert snp folder to back to its previous SNP folder if the previous task fail + if: failure() && steps.install-snp-host-kernel.outcome != 'success' + run: | + mv ${HOME}/snp ${HOME}/latest_snp + mv ${HOME}/previous_snp ${HOME}/snp + + reboot_self_hosted_runner: + runs-on: self-hosted + needs: snp_setup_host + steps: + - name: Reboot + timeout-minutes: 8 + run: sudo reboot + shell: bash + + wait_time_after_reboot: + runs-on: ubuntu-latest + needs: reboot_self_hosted_runner + timeout-minutes: 30 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Sleep for 5 minutes + timeout-minutes: 15 + run: sleep 300s + shell: bash + + test_launch_snp_guest: + runs-on: self-hosted + needs: wait_time_after_reboot + timeout-minutes: 30 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set the next available guest network port number + run: | + export DOTENV_PATH="${HOME}/.env" + echo "guest_port_in_use=$(python ./.github/workflows/handle_guest_network_ports.py get-next-available-port-number)" >> $GITHUB_ENV + + - name: Test the launch of sample SNP guest on the self-hosted runner + run: | + # Uses sev utility script to test the launch of latest SNP guest + wget https://raw.githubusercontent.com/LakshmiSaiHarika/sev-utils/Fedora-Latest-SNP-kernel-Upstream/tools/snp.sh + chmod +x snp.sh + + export GUEST_NAME="sample-snp-guest" + export HOST_SSH_PORT=${{ env.guest_port_in_use }} + + ./snp.sh launch-guest + + - name: Stop the active running SNP guest for this PR + if: success() || failure() + continue-on-error: true + run: | + export GUEST_NAME="sample-snp-guest" + export HOST_SSH_PORT=${{ env.guest_port_in_use }} + + ./snp.sh stop-guests + + - name: Remove current active guest network port from GHAW network port file + if: success() || failure() + run: | + export DOTENV_PATH="${HOME}/.env" + python ./.github/workflows/handle_guest_network_ports.py remove-ghaw-used-port-number ${{ env.guest_port_in_use }} + + - name: Cleanup SNP guest folder + if: success() || failure() + run: | + rm -rf ${HOME}/snp/launch/sample-snp-guest + ssh-keygen -R [localhost]:${{ env.guest_port_in_use }} + + trigger_pr_tests: + runs-on: self-hosted + needs: test_launch_snp_guest + timeout-minutes: 30 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Trigger all open Virtee CI PR tests(sev, snphost, snpguest) + run: | + export DOTENV_PATH="${HOME}/.env" + python ./.github/workflows/trigger_pr_tests.py diff --git a/.github/workflows/trigger_pr_tests.py b/.github/workflows/trigger_pr_tests.py new file mode 100644 index 00000000..2b2d98a9 --- /dev/null +++ b/.github/workflows/trigger_pr_tests.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: Apache-2.0 + +''' +Triggers PR CI test workflows on all the open PRs for the given GH owner, GH repository and GH Workflow ID +This script is used as an event after SNP kernel update on the self-hosted runner + +Pre-requisite for this tool use: + Set DOTENV_PATH(environment variable) on the host with the .env file path having VIRTEE_API_TOKEN +''' + +import requests +import time +from dotenv import load_dotenv +import os + +def trigger_open_pr_tests(owner, repo, workflow_id): + ''' + Activates GH Workflow for the given GH owner, GH repo and GH Action workflow ID + ''' + # Loads Virtee Repository PAT for GH Action API use to trigger GH workflow + dotenv_path = os.path.join(os.path.dirname(__file__), os.getenv("DOTENV_PATH")) + load_dotenv(dotenv_path) + virtee_api_token = os.getenv("VIRTEE_API_TOKEN") + + # Constructs the API URL + workflow_url = f"https://api.github.com/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches" + + # Sets header with authorization and API version + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {virtee_api_token}", + "X-GitHub-Api-Version": "2022-11-28" + } + + open_pr_url = f"https://api.github.com/repos/{owner}/{repo}/pulls?state=open" + headers = {"Authorization": f"token {virtee_api_token}"} + + # Gets all open PR list for the given GH owner, GH repo and GH workflow ID + all_open_prs = requests.get(open_pr_url, headers=headers) + + # Activate Virtee CI PR test workflow for all open PRs via GH Action API + if all_open_prs.status_code == 200: + prs = all_open_prs.json() + for pr in prs: + + pr_number = pr['html_url'].split("pull/")[-1] + pr_source_branch = pr['head']['ref'] + pr_inputs = {"pull_request_number": pr_number,"pull_request_branch":pr_source_branch } + + # Prepares the data payload with branch name and inputs + pr_post_data = { + "ref": "main", + "inputs": pr_inputs + } + + print(f"\nTriggers {repo} PR CI test for PR #{pr_number} and PR source branch {pr_source_branch}") + pr_ci_test = requests.post(workflow_url, headers=headers, json=pr_post_data) + + # Handles the response + if pr_ci_test.status_code != 204: + print(f"ERROR: {pr_ci_test.status_code}" + " - " + f"{repo} PR CI test workflow request fails for PR #{pr_number}(PR source branch:{pr_source_branch})") + print(f"ERROR CAUSE: {pr_ci_test.json()}") + else: + print(f"\n ERROR: {all_open_prs.status_code}" + " - " + f"GET request to all the {repo} open PRs list fails!") + print(f"ERROR CAUSE: {all_open_prs.json()}") + print(f"\n") + +def main(): + # Initializes Virtee Repositories list + virtee_owner = "virtee" + + virtee_repo_workflows= { + "sev":"sev_ci_pr_test.yaml", + "snphost":"snphost_ci_test.yaml", + "snpguest":"snpguest_ci_pr_test.yaml" + } + + # Trigger all open PR CI tests for all Virtee Repositories + for repo, workflow in virtee_repo_workflows.items(): + trigger_open_pr_tests(virtee_owner, repo, workflow) + +if __name__ == '__main__': + main() +