diff --git a/.github/workflows/handle_guest_network_ports.py b/.github/workflows/handle_guest_network_ports.py new file mode 100644 index 0000000..9128be3 --- /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/snp_function_declarations.sh b/.github/workflows/snp_function_declarations.sh new file mode 100644 index 0000000..e7eb0b4 --- /dev/null +++ b/.github/workflows/snp_function_declarations.sh @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 + +#!/bin/bash + +verify_snp_host() { +local AMDSEV_URL="https://github.com/LakshmiSaiHarika/AMDSEV.git" +local AMDSEV_DEFAULT_BRANCH="fedora-build-install-upstream-kernel" + +# Checks if SNP is enabled on the SNP host Kernel +if ! sudo dmesg | grep -i "SEV-SNP enabled" 2>&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/snpguest_ci_pr_test.yaml b/.github/workflows/snpguest_ci_pr_test.yaml new file mode 100644 index 0000000..b380ce6 --- /dev/null +++ b/.github/workflows/snpguest_ci_pr_test.yaml @@ -0,0 +1,166 @@ +name: snpguest 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_check_snp: + 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 + + snp_guest_tests: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Sleep for 15 seconds + run: sleep 15 + + - 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-${{ env.pr_number }}" + echo "guest_name=snp-guest-${{ 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 snpguest tool PR cargo test on the guest(without flags) + run: | + set -e + + source ./.github/workflows/snp_function_declarations.sh + + # Install snpguest dependencies as a root user + ssh_guest_command "sudo su - </dev/null + sudo dnf install -y git gcc + sudo dnf groupinstall -y 'Development Tools' + sudo dnf groupinstall -y 'Development Libraries' + sudo dnf install -y pkgconf perl-FindBin perl-IPC-Cmd openssl-devel + EOF" ${{ env.guest_name }} ${{ env.guest_port_in_use }} + + # Perform PR test on snpguest tool as root user + ssh_guest_command "sudo su - <