-
Notifications
You must be signed in to change notification settings - Fork 39
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
base: main
Are you sure you want to change the base?
Changes from all commits
64f6c81
5c7e170
34d7663
d2e6d09
527b19b
fe9f6bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() | ||
|
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 | ||
chmod +x snp.sh | ||
|
||
export GUEST_NAME=${{ env.guest_name }} | ||
export HOST_SSH_PORT=${{ env.guest_port_in_use }} | ||
|
||
./snp.sh launch-guest | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can use:
|
||
|
||
- 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 )) }} | ||
|
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 | ||
} | ||
|
There was a problem hiding this comment.
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