Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SEV CI PR test for SNP host and guest on the self-hosted runner #233

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
117 changes: 117 additions & 0 deletions .github/workflows/handle_guest_network_ports.py
Original file line number Diff line number Diff line change
@@ -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()

207 changes: 207 additions & 0 deletions .github/workflows/sev_ci_pr_test.yaml
Original file line number Diff line number Diff line change
@@ -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
Copy link
Author

@LakshmiSaiHarika LakshmiSaiHarika Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change into sev-utils upstream URL

upstream link

chmod +x snp.sh

export GUEST_NAME=${{ env.guest_name }}
export HOST_SSH_PORT=${{ env.guest_port_in_use }}

./snp.sh launch-guest
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use
./snp.sh launch-guest --guest-name "${{ env.guest_name }}" --guest-port "${{ env.guest_port_in_use }}"


- 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 - <<EOF
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -sSf | sh -s -- -y
source "/home/root/.cargo/env" 2>/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 - <<EOF
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 test
EOF" ${{ env.guest_name }} ${{ env.guest_port_in_use }}

- name: Stop the active running SNP guest for this PR
if: success() || failure()
continue-on-error: true
run: |
export GUEST_NAME=${{ env.guest_name }}
export HOST_SSH_PORT=${{ env.guest_port_in_use }}

./snp.sh stop-guests
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use:

./snp.sh stop-guests --guest-name "${{ env.guest_name }}"


- 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/${{ env.guest_name }}
ssh-keygen -R [localhost]:${{ env.guest_port_in_use }}

# Update this workflow title dynamically with PR details
run-name: |
${{ (startsWith(github.event_name, 'workflow_dispatch') && format('sev PR CI test for PR #{0}/PR source branch({1})', github.event.inputs.pull_request_number, github.event.inputs.pull_request_branch)) ||
(startsWith(github.event_name, 'pull_request') && format('{0}', github.event.pull_request.title )) }}

69 changes: 69 additions & 0 deletions .github/workflows/snp_function_declarations.sh
Original file line number Diff line number Diff line change
@@ -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
}

Loading