diff --git a/.github/workflows/mshv-build.yaml b/.github/workflows/mshv-build.yaml new file mode 100644 index 000000000..ba6cca89a --- /dev/null +++ b/.github/workflows/mshv-build.yaml @@ -0,0 +1,78 @@ +name: MSHV Build +on: + workflow_dispatch: +jobs: + infrasetup: + uses: ./.github/workflows/mshv-infra.yaml + name: MSHV Infra Setup + with: + # PR_NUMBER: ${{ github.event.pull_request.number }} + RG: "MSHVBuild" + SKU: "Standard_d16s_v5" + secrets: + MSHV_MI_CLIENT_ID: ${{ secrets.MSHV_MI_CLIENT_ID }} + MSHV_STORAGE_ACCOUNT_PATHS: ${{ secrets.MSHV_STORAGE_ACCOUNT_PATHS }} + MSHV_USERNAME: ${{ secrets.MSHV_USERNAME }} + MSHV_PASSWORD: ${{ secrets.MSHV_PASSWORD }} + MSHV_X86_SOURCE_PATH: ${{ secrets.MSHV_X86_SOURCE_PATH }} + MSHV_RUNNER_RG: ${{ secrets.MSHV_RUNNER_RG }} + MSHV_RUNNER: ${{ secrets.MSHV_RUNNER }} + build: + name: Build + runs-on: + - self-hosted + - Linux + needs: infrasetup + strategy: + fail-fast: false + matrix: + RUNNER: + - 'test-runner' + BUILD_CMD: + - 'cargo build' + - 'cargo build --features "mshv"' + - 'cargo build --features "mshv,sev_snp,igvm"' + - 'cargo build --package vhost_user_net' + - 'cargo build --package vhost_user_block' + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: AZ Login + run: az login --identity --client-id ${{ secrets.MSHV_MI_CLIENT_ID }} + + - name: Build MSHV + run: | + set -x + echo "Retrieving VM Private IP address" + PRIVATE_IP=$(az vm show -g MSHVBuild-${GITHUB_RUN_ID} -n x86_${GITHUB_RUN_ID} -d --query privateIps -o tsv) + if [ $? -ne 0 ]; then + echo "Failed to retrieve private IP address" + exit 1 + fi + echo "Successfully retrieved private IP address" + echo $PRIVATE_IP + + sshpass -p "${{ secrets.MSHV_PASSWORD }}" ssh -o StrictHostKeyChecking=no "${{ secrets.MSHV_USERNAME }}"@$PRIVATE_IP << EOF + set -x + echo "Logged in" + sudo tdnf install git moby-engine moby-cli dosfstools mtools build-essential m4 bison flex libuuid-devel qemu-img -y + curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable --profile default -y + export PATH="\$HOME/.cargo/bin:$PATH" + cd cloud-hypervisor + # Build Cloud-Hypervisor + echo "Building Cloud-Hypervisor" + ${{ matrix.BUILD_CMD }} --release + EOF + + cleanup: + needs: build + if: always() + runs-on: + - self-hosted + - Linux + steps: + - name: Delete Resource Group + run: | + az group delete --name MSHVBuild-${GITHUB_RUN_ID} --yes --no-wait + echo "Resource group deleted" \ No newline at end of file diff --git a/.github/workflows/mshv-infra.yaml b/.github/workflows/mshv-infra.yaml new file mode 100644 index 000000000..706b36d3a --- /dev/null +++ b/.github/workflows/mshv-infra.yaml @@ -0,0 +1,229 @@ +name: MSHV Infra Setup +on: + workflow_call: + inputs: + ARCH: + description: 'Architecture for the VM' + required: true + type: string + KEY: + description: 'SSH Key Name' + required: true + type: string + OS_DISK_SIZE: + description: 'OS Disk Size in GB' + required: true + type: string + RG: + description: 'Resource Group Name' + required: true + type: string + VM_SKU: + description: 'VM SKU' + required: true + type: string + secrets: + MI_CLIENT_ID: + required: true + RUNNER_RG: + required: true + STORAGE_ACCOUNT_PATHS: + required: true + ARCH_SOURCE_PATH: + required: true + USERNAME: + required: true + outputs: + PRIVATE_IP: + description: 'Private IP of the VM' + value: ${{ jobs.infra-setup.outputs.PRIVATE_IP }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + infra-setup: + name: ${{ inputs.ARCH }} VM Provision + runs-on: + - self-hosted + - Linux + outputs: + PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }} + steps: + - name: Install & login to AZ CLI + env: + MI_CLIENT_ID: ${{ secrets.MI_CLIENT_ID }} + run: | + set -e + echo "Installing Azure CLI if not already installed" + if ! command -v az &>/dev/null; then + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + else + echo "Azure CLI already installed" + fi + az --version + echo "Logging into Azure CLI using Managed Identity" + az login --identity --client-id ${MI_CLIENT_ID} + + - name: Get Location + id: get-location + env: + SKU: ${{ inputs.VM_SKU }} + STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }} + run: | + set -e + # Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2) + vcpu=$(echo "$SKU" | sed -n 's/^Standard_[A-Za-z]\+\([0-9]\+\).*/\1/p') + if [[ -z "$vcpu" ]]; then + echo "Cannot extract vCPU count from SKU: $SKU" + exit 1 + fi + + SUPPORTED_LOCATIONS=$(echo "$STORAGE_ACCOUNT_PATHS" | jq -r 'to_entries[] | .key') + + for location in $SUPPORTED_LOCATIONS; do + family=$(az vm list-skus --size "$SKU" --location "$location" --resource-type "virtualMachines" --query '[0].family' -o tsv) + if [[ -z "$family" ]]; then + echo "Cannot determine VM family for SKU: $SKU in $location" + continue + fi + + usage=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json) + current=$(echo "$usage" | jq -r '.currentValue') + limit=$(echo "$usage" | jq -r '.limit') + + if [[ $((limit - current)) -ge $vcpu ]]; then + echo "Sufficient quota found in $location" + echo "location=$location" >> "$GITHUB_OUTPUT" + exit 0 + fi + done + + echo "No location found with sufficient vCPU quota for SKU: $SKU" + exit 1 + + - name: Create Resource Group + id: rg-setup + env: + LOCATION: ${{ steps.get-location.outputs.location }} + RG: ${{ inputs.RG }} + STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }} + run: | + set -e + echo "Creating Resource Group: $RG" + # Create the resource group + echo "Creating resource group in location: ${LOCATION}" + az group create --name ${RG} --location ${LOCATION} + echo "Resource group created successfully." + + - name: Generate SSH Key + id: generate-ssh-key + env: + KEY: ${{ inputs.KEY }} + run: | + set -e + echo "Generating SSH key: $KEY" + mkdir -p ~/.ssh + ssh-keygen -t rsa -b 4096 -f ~/.ssh/${KEY} -N "" + + - name: Create VM + id: vm-setup + env: + KEY: ${{ inputs.KEY }} + LOCATION: ${{ steps.get-location.outputs.location }} + OS_DISK_SIZE: ${{ inputs.OS_DISK_SIZE }} + RG: ${{ inputs.RG }} + RUNNER_RG: ${{ secrets.RUNNER_RG }} + USERNAME: ${{ secrets.USERNAME }} + VM_SKU: ${{ inputs.VM_SKU }} + VM_IMAGE_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_image + VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }} + run: | + set -e + echo "Creating $VM_SKU VM: $VM_NAME" + + # Extract subnet ID from the runner VM + echo "Retrieving subnet ID..." + SUBNET_ID=$(az network vnet list --resource-group ${RUNNER_RG} --query "[?contains(location, '${LOCATION}')].{SUBNETS:subnets}" | jq -r ".[0].SUBNETS[0].id") + if [[ -z "${SUBNET_ID}" ]]; then + echo "ERROR: Failed to retrieve Subnet ID." + exit 1 + fi + + # Extract image ID from the runner VM + echo "Retrieving image ID..." + IMAGE_ID=$(az image show --resource-group ${RUNNER_RG} --name ${VM_IMAGE_NAME} --query "id" -o tsv) + if [[ -z "${IMAGE_ID}" ]]; then + echo "ERROR: Failed to retrieve Image ID." + exit 1 + fi + + # Create VM + az vm create \ + --resource-group ${RG} \ + --name ${VM_NAME} \ + --subnet ${SUBNET_ID} \ + --size ${VM_SKU} \ + --location ${LOCATION} \ + --image ${IMAGE_ID} \ + --os-disk-size-gb ${OS_DISK_SIZE} \ + --public-ip-sku Standard \ + --storage-sku Premium_LRS \ + --public-ip-address "" \ + --admin-username ${USERNAME} \ + --ssh-key-value ~/.ssh/${KEY}.pub \ + --security-type Standard \ + --output json + + echo "VM creation process completed successfully." + + - name: Get VM Private IP + id: get-vm-ip + env: + RG: ${{ inputs.RG }} + VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }} + run: | + set -e + echo "Retrieving VM Private IP address..." + # Retrieve VM Private IP address + PRIVATE_IP=$(az vm show -g ${RG} -n ${VM_NAME} -d --query privateIps -o tsv) + if [[ -z "$PRIVATE_IP" ]]; then + echo "ERROR: Failed to retrieve private IP address." + exit 1 + fi + echo "PRIVATE_IP=$PRIVATE_IP" >> $GITHUB_OUTPUT + + - name: Wait for SSH availability + env: + KEY: ${{ inputs.KEY }} + PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }} + USERNAME: ${{ secrets.USERNAME }} + run: | + echo "Waiting for SSH to be accessible..." + timeout 120 bash -c 'until ssh -o StrictHostKeyChecking=no -i ~/.ssh/${KEY} ${USERNAME}@${PRIVATE_IP} "exit" 2>/dev/null; do sleep 5; done' + echo "VM is accessible!" + + - name: Remove Old Host Key + env: + PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }} + run: | + set -e + echo "Removing the old host key" + ssh-keygen -R $PRIVATE_IP + + - name: SSH into VM and Install Dependencies + env: + KEY: ${{ inputs.KEY }} + PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }} + USERNAME: ${{ secrets.USERNAME }} + run: | + set -e + ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF + set -e + echo "Logged in successfully." + echo "Installing dependencies..." + sudo tdnf install -y git moby-engine moby-cli clang llvm pkg-config make gcc glibc-devel + echo "Installing Rust..." + curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable --profile default -y + export PATH="\$HOME/.cargo/bin:\$PATH" + cargo --version + EOF \ No newline at end of file diff --git a/.github/workflows/mshv-integration.yaml b/.github/workflows/mshv-integration.yaml new file mode 100644 index 000000000..f965f66eb --- /dev/null +++ b/.github/workflows/mshv-integration.yaml @@ -0,0 +1,95 @@ +name: MSHV Integration Tests +on: + pull_request: + workflow_dispatch: + inputs: + branch: + description: 'Branch to run integration tests on' + required: true + default: 'main' + +jobs: + infra-setup: + name: MSHV Infra Setup (x86_64) + uses: ./.github/workflows/mshv-infra.yaml + with: + ARCH: x86_64 + KEY: azure_key_${{ github.run_id }} + OS_DISK_SIZE: 512 + RG: MSHV-INTEGRATION-${{ github.run_id }} + VM_SKU: Standard_D16s_v5 + secrets: + MI_CLIENT_ID: ${{ secrets.MSHV_MI_CLIENT_ID }} + RUNNER_RG: ${{ secrets.MSHV_RUNNER_RG }} + STORAGE_ACCOUNT_PATHS: ${{ secrets.MSHV_STORAGE_ACCOUNT_PATHS }} + ARCH_SOURCE_PATH: ${{ secrets.MSHV_X86_SOURCE_PATH }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + + run_tests: + name: Integration Tests + needs: infra-setup + if: ${{ always() && needs.infra-setup.result == 'success' }} + runs-on: + - self-hosted + - Linux + steps: + - name: Determine branch to build + run: | + echo "Determining branch to build and test..." + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "BRANCH=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + else + echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV + fi + + - name: Run integration tests + env: + BRANCH_NAME: ${{ env.BRANCH }} + KEY: azure_key_${{ github.run_id }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }} + RG: MSHV-${{ github.run_id }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + run: | + set -e + echo "Connecting to the VM via SSH..." + ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF + set -e + echo "Logged in successfully." + export PATH="\$HOME/.cargo/bin:\$PATH" + echo "${BRANCH_NAME}" + git clone --depth 1 --single-branch --branch "$BRANCH_NAME" https://github.com/cloud-hypervisor/cloud-hypervisor.git + cd cloud-hypervisor + sudo ./scripts/dev_cli.sh tests --hypervisor mshv --integration + EOF + + cleanup: + name: Cleanup + needs: build-test + if: always() + runs-on: + - self-hosted + - Linux + steps: + - name: Delete RG + env: + RG: MSHV-INTEGRATION-${{ github.run_id }} + run: | + if az group exists --name ${RG}; then + az group delete --name ${RG} --yes --no-wait + else + echo "Resource Group ${RG} does not exist. Skipping deletion." + fi + echo "Cleanup process completed." + + - name: Delete SSH Key + env: + KEY: azure_key_${{ github.run_id }} + run: | + if [ -f ~/.ssh/${KEY} ]; then + rm -f ~/.ssh/${KEY} ~/.ssh/${KEY}.pub + echo "SSH key deleted successfully." + else + echo "SSH key does not exist. Skipping deletion." + fi + echo "Cleanup process completed." \ No newline at end of file diff --git a/.github/workflows/mshv-live-migration.yml b/.github/workflows/mshv-live-migration.yml new file mode 100644 index 000000000..1b6b8510e --- /dev/null +++ b/.github/workflows/mshv-live-migration.yml @@ -0,0 +1,95 @@ +name: MSHV Integration Tests +on: + pull_request: + workflow_dispatch: + inputs: + branch: + description: 'Branch to run live-migration tests on' + required: true + default: 'main' + +jobs: + infra-setup: + name: MSHV Infra Setup (x86_64) + uses: ./.github/workflows/mshv-infra.yaml + with: + ARCH: x86_64 + KEY: azure_key_${{ github.run_id }} + OS_DISK_SIZE: 512 + RG: MSHV-LIVE-MIGRATION-${{ github.run_id }} + VM_SKU: Standard_D16s_v5 + secrets: + MI_CLIENT_ID: ${{ secrets.MSHV_MI_CLIENT_ID }} + RUNNER_RG: ${{ secrets.MSHV_RUNNER_RG }} + STORAGE_ACCOUNT_PATHS: ${{ secrets.MSHV_STORAGE_ACCOUNT_PATHS }} + ARCH_SOURCE_PATH: ${{ secrets.MSHV_X86_SOURCE_PATH }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + + run_tests: + name: Integration Tests + needs: infra-setup + if: ${{ always() && needs.infra-setup.result == 'success' }} + runs-on: + - self-hosted + - Linux + steps: + - name: Determine branch to build + run: | + echo "Determining branch to build and test..." + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "BRANCH=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + else + echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV + fi + + - name: Run live-migration tests + env: + BRANCH_NAME: ${{ env.BRANCH }} + KEY: azure_key_${{ github.run_id }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }} + RG: MSHV-${{ github.run_id }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + run: | + set -e + echo "Connecting to the VM via SSH..." + ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF + set -e + echo "Logged in successfully." + export PATH="\$HOME/.cargo/bin:\$PATH" + echo "${BRANCH_NAME}" + git clone --depth 1 --single-branch --branch "$BRANCH_NAME" https://github.com/cloud-hypervisor/cloud-hypervisor.git + cd cloud-hypervisor + sudo ./scripts/dev_cli.sh tests --hypervisor mshv --live-migration + EOF + + cleanup: + name: Cleanup + needs: build-test + if: always() + runs-on: + - self-hosted + - Linux + steps: + - name: Delete RG + env: + RG: MSHV-LIVE-MIGRATION-${{ github.run_id }} + run: | + if az group exists --name ${RG}; then + az group delete --name ${RG} --yes --no-wait + else + echo "Resource Group ${RG} does not exist. Skipping deletion." + fi + echo "Cleanup process completed." + + - name: Delete SSH Key + env: + KEY: azure_key_${{ github.run_id }} + run: | + if [ -f ~/.ssh/${KEY} ]; then + rm -f ~/.ssh/${KEY} ~/.ssh/${KEY}.pub + echo "SSH key deleted successfully." + else + echo "SSH key does not exist. Skipping deletion." + fi + echo "Cleanup process completed." \ No newline at end of file diff --git a/.github/workflows/mshv-perf-metrics.yaml b/.github/workflows/mshv-perf-metrics.yaml new file mode 100644 index 000000000..b483483aa --- /dev/null +++ b/.github/workflows/mshv-perf-metrics.yaml @@ -0,0 +1,95 @@ +name: MSHV Integration Tests +on: + pull_request: + workflow_dispatch: + inputs: + branch: + description: 'Branch to run perf metrics tests on' + required: true + default: 'main' + +jobs: + infra-setup: + name: MSHV Infra Setup (x86_64) + uses: ./.github/workflows/mshv-infra.yaml + with: + ARCH: x86_64 + KEY: azure_key_${{ github.run_id }} + OS_DISK_SIZE: 512 + RG: MSHV-METRICS-${{ github.run_id }} + VM_SKU: Standard_D16s_v5 + secrets: + MI_CLIENT_ID: ${{ secrets.MSHV_MI_CLIENT_ID }} + RUNNER_RG: ${{ secrets.MSHV_RUNNER_RG }} + STORAGE_ACCOUNT_PATHS: ${{ secrets.MSHV_STORAGE_ACCOUNT_PATHS }} + ARCH_SOURCE_PATH: ${{ secrets.MSHV_X86_SOURCE_PATH }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + + run_tests: + name: Integration Tests + needs: infra-setup + if: ${{ always() && needs.infra-setup.result == 'success' }} + runs-on: + - self-hosted + - Linux + steps: + - name: Determine branch to build + run: | + echo "Determining branch to build and test..." + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "BRANCH=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + else + echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV + fi + + - name: Run perf metrics tests + env: + BRANCH_NAME: ${{ env.BRANCH }} + KEY: azure_key_${{ github.run_id }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }} + RG: MSHV-${{ github.run_id }} + USERNAME: ${{ secrets.MSHV_USERNAME }} + run: | + set -e + echo "Connecting to the VM via SSH..." + ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF + set -e + echo "Logged in successfully." + export PATH="\$HOME/.cargo/bin:\$PATH" + echo "${BRANCH_NAME}" + git clone --depth 1 --single-branch --branch "$BRANCH_NAME" https://github.com/cloud-hypervisor/cloud-hypervisor.git + cd cloud-hypervisor + sudo ./scripts/dev_cli.sh tests --hypervisor mshv --metrics + EOF + + cleanup: + name: Cleanup + needs: build-test + if: always() + runs-on: + - self-hosted + - Linux + steps: + - name: Delete RG + env: + RG: MSHV-METRICS-${{ github.run_id }} + run: | + if az group exists --name ${RG}; then + az group delete --name ${RG} --yes --no-wait + else + echo "Resource Group ${RG} does not exist. Skipping deletion." + fi + echo "Cleanup process completed." + + - name: Delete SSH Key + env: + KEY: azure_key_${{ github.run_id }} + run: | + if [ -f ~/.ssh/${KEY} ]; then + rm -f ~/.ssh/${KEY} ~/.ssh/${KEY}.pub + echo "SSH key deleted successfully." + else + echo "SSH key does not exist. Skipping deletion." + fi + echo "Cleanup process completed." \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 346f40fad..8fcd832f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,9 +360,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" diff --git a/Cargo.toml b/Cargo.toml index 9714ab4f8..4ceb0aa56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ serde_with = { version = "3.14.0", default-features = false } anyhow = "1.0.98" bitflags = "2.9.0" byteorder = "1.5.0" -cfg-if = "1.0.0" +cfg-if = "1.0.1" clap = "4.5.13" dhat = "0.3.3" dirs = "6.0.0"