mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf94d3dad9 | ||
|
|
02e3570bdd | ||
|
|
dae66ce493 | ||
|
|
90cee24f98 | ||
|
|
5a0b6f2d06 | ||
|
|
30166a4ea5 | ||
|
|
f93340d337 | ||
|
|
4c1f854ee9 | ||
|
|
ecab9f1b96 |
@@ -1,25 +0,0 @@
|
|||||||
# https://editorconfig.org/
|
|
||||||
#
|
|
||||||
# Hints for editors to assist with correct formatting as you type.
|
|
||||||
root = true
|
|
||||||
|
|
||||||
# Unix-style newlines with a newline ending every file
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
# Recommendation, not enforced.
|
|
||||||
max_line_length = 80
|
|
||||||
|
|
||||||
[Makefile]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
# Inherited as default
|
|
||||||
# [*.sh]
|
|
||||||
# indent_size = 4
|
|
||||||
|
|
||||||
[{Cargo.lock,*.md,*.toml,*.yml,*.yaml}]
|
|
||||||
indent_size = 2
|
|
||||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -37,11 +37,6 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: all
|
- dependency-type: all
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
semver-major-days: 14
|
|
||||||
semver-minor-days: 7
|
|
||||||
semver-patch-days: 3
|
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "acpi_tables"
|
- dependency-name: "acpi_tables"
|
||||||
- dependency-name: "kvm-bindings"
|
- dependency-name: "kvm-bindings"
|
||||||
|
|||||||
16
.github/workflows/audit.yaml
vendored
Normal file
16
.github/workflows/audit.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Cloud Hypervisor Dependency Audit
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security_audit:
|
||||||
|
name: Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions-rust-lang/audit@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
77
.github/workflows/build.yaml
vendored
Normal file
77
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: Cloud Hypervisor Build
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- stable
|
||||||
|
- beta
|
||||||
|
- nightly
|
||||||
|
- "1.89.0"
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install musl-gcc
|
||||||
|
run: sudo apt install -y musl-tools
|
||||||
|
|
||||||
|
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build (default features)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor
|
||||||
|
|
||||||
|
- name: Build (kvm)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm"
|
||||||
|
|
||||||
|
- name: Build (default features + tdx)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "tdx"
|
||||||
|
|
||||||
|
- name: Build (default features + dbus_api)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "dbus_api"
|
||||||
|
|
||||||
|
- name: Build (default features + guest_debug)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "guest_debug"
|
||||||
|
|
||||||
|
- name: Build (default features + pvmemcontrol)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol"
|
||||||
|
|
||||||
|
- name: Build (default features + fw_cfg)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg"
|
||||||
|
|
||||||
|
- name: Build (default features + ivshmem)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --features "ivshmem"
|
||||||
|
|
||||||
|
- name: Build (mshv)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv"
|
||||||
|
|
||||||
|
- name: Build (sev_snp)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "sev_snp"
|
||||||
|
|
||||||
|
- name: Build (igvm)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "igvm"
|
||||||
|
|
||||||
|
- name: Build (mshv + kvm)
|
||||||
|
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm"
|
||||||
|
|
||||||
|
- name: Release Build (default features)
|
||||||
|
run: cargo build --locked --all --release --target=${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Check build did not modify any files
|
||||||
|
run: test -z "$(git status --porcelain)"
|
||||||
770
.github/workflows/ci.yaml
vendored
770
.github/workflows/ci.yaml
vendored
@@ -1,770 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on: [pull_request, merge_group]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
preflight:
|
|
||||||
name: preflight
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
full: ${{ steps.classify.outputs.full }}
|
|
||||||
rust: ${{ steps.changes.outputs.rust }}
|
|
||||||
cargo: ${{ steps.changes.outputs.cargo }}
|
|
||||||
openapi: ${{ steps.changes.outputs.openapi }}
|
|
||||||
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
|
||||||
shell: ${{ steps.changes.outputs.shell }}
|
|
||||||
ci: ${{ steps.changes.outputs.ci }}
|
|
||||||
docs: ${{ steps.changes.outputs.docs }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- id: changes
|
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
rust:
|
|
||||||
- '**/*.rs'
|
|
||||||
- 'build.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- 'rust-toolchain.toml'
|
|
||||||
cargo:
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
openapi:
|
|
||||||
- 'vmm/src/api/openapi/**'
|
|
||||||
dockerfile:
|
|
||||||
- 'resources/Dockerfile'
|
|
||||||
shell:
|
|
||||||
- '**/*.sh'
|
|
||||||
- 'scripts/**'
|
|
||||||
ci:
|
|
||||||
- '.github/workflows/**'
|
|
||||||
docs:
|
|
||||||
- 'docs/**'
|
|
||||||
- '**/*.md'
|
|
||||||
- '.github/ISSUE_TEMPLATE/**'
|
|
||||||
- 'LICENSES/**'
|
|
||||||
- 'CODEOWNERS'
|
|
||||||
- id: classify
|
|
||||||
name: Classify changes
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
full=false
|
|
||||||
if [[ "${{ steps.changes.outputs.rust }}" == "true" \
|
|
||||||
|| "${{ steps.changes.outputs.dockerfile }}" == "true" \
|
|
||||||
|| "${{ steps.changes.outputs.shell }}" == "true" \
|
|
||||||
|| "${{ steps.changes.outputs.ci }}" == "true" ]]; then
|
|
||||||
full=true
|
|
||||||
fi
|
|
||||||
echo "full=$full" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "full=$full"
|
|
||||||
dco:
|
|
||||||
name: dco
|
|
||||||
needs: [preflight]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Set up Python 3.x
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
- name: Check DCO
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
pip3 install -U dco-check
|
|
||||||
dco-check -e "49699333+dependabot[bot]@users.noreply.github.com"
|
|
||||||
gitlint:
|
|
||||||
name: gitlint
|
|
||||||
needs: [preflight]
|
|
||||||
# PR-only: gitlint needs GITHUB_BASE_REF, unset on merge_group.
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
# PR head, not the merge ref, so gitlint sees the PR's commits.
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up Python 3.10
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.10"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install --upgrade gitlint
|
|
||||||
- name: Lint git commit messages
|
|
||||||
run: |
|
|
||||||
gitlint --commits "origin/$GITHUB_BASE_REF.."
|
|
||||||
lychee:
|
|
||||||
name: lychee
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.docs == 'true' || needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Get changed files in PR
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
|
||||||
with:
|
|
||||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
|
||||||
- name: Verify Changed Files
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
echo "--- tj-actions/changed-files Outputs ---"
|
|
||||||
echo "any_changed: ${{ steps.changed-files.outputs.any_changed }}"
|
|
||||||
echo "all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}"
|
|
||||||
echo "added_files: ${{ steps.changed-files.outputs.added_files }}"
|
|
||||||
echo "modified_files: ${{ steps.changed-files.outputs.modified_files }}"
|
|
||||||
echo "deleted_files: ${{ steps.changed-files.outputs.deleted_files }}"
|
|
||||||
echo "renamed_files: ${{ steps.changed-files.outputs.renamed_files }}"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
if [ -n "${{ steps.changed-files.outputs.all_changed_files }}" ]; then
|
|
||||||
echo "Detected changes: all_changed_files output is NOT empty."
|
|
||||||
else
|
|
||||||
echo "No changes detected: all_changed_files output IS empty."
|
|
||||||
fi
|
|
||||||
- name: Link Availability Check (Diff Only)
|
|
||||||
if: ${{ steps.changed-files.outputs.all_changed_files != '' }}
|
|
||||||
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0
|
|
||||||
with:
|
|
||||||
args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }}
|
|
||||||
failIfEmpty: false
|
|
||||||
fail: true
|
|
||||||
taplo:
|
|
||||||
name: taplo
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.cargo == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev
|
|
||||||
- name: Install taplo
|
|
||||||
run: cargo install taplo-cli --locked
|
|
||||||
- name: Check formatting
|
|
||||||
run: taplo fmt --check
|
|
||||||
audit:
|
|
||||||
name: audit
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.cargo == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: actions-rust-lang/audit@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
shlint:
|
|
||||||
name: shlint
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.shell == 'true' || needs.preflight.outputs.ci == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Run the shell script checkers
|
|
||||||
uses: luizm/action-sh-checker@883217215b11c1fabbf00eb1a9a041f62d74c744 # v0.10.0
|
|
||||||
env:
|
|
||||||
SHFMT_OPTS: -i 4 -d
|
|
||||||
SHELLCHECK_OPTS: -x --source-path scripts
|
|
||||||
hadolint:
|
|
||||||
name: hadolint
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.dockerfile == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Lint Dockerfile
|
|
||||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
|
||||||
with:
|
|
||||||
dockerfile: ./resources/Dockerfile
|
|
||||||
format: tty
|
|
||||||
no-fail: false
|
|
||||||
verbose: true
|
|
||||||
failure-threshold: info
|
|
||||||
reuse:
|
|
||||||
name: reuse
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true' || needs.preflight.outputs.cargo == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: REUSE Compliance Check
|
|
||||||
uses: fsfe/reuse-action@v6
|
|
||||||
formatting:
|
|
||||||
name: formatting
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
rust: [nightly]
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- aarch64-unknown-linux-musl
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
components: rustfmt
|
|
||||||
- name: Formatting (rustfmt)
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
- name: Formatting (fuzz) (rustfmt)
|
|
||||||
run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check
|
|
||||||
package-consistency:
|
|
||||||
name: package-consistency
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt install -y python3
|
|
||||||
- name: Install Rust toolchain stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
- name: Check Rust VMM Package Consistency of root Workspace
|
|
||||||
run: python3 scripts/package-consistency-check.py github.com/rust-vmm
|
|
||||||
- name: Check Rust VMM Package Consistency of fuzz Workspace
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
pushd fuzz
|
|
||||||
python3 ../scripts/package-consistency-check.py github.com/rust-vmm
|
|
||||||
popd
|
|
||||||
fuzz-build:
|
|
||||||
name: fuzz-build
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
rust: [nightly]
|
|
||||||
target: [x86_64-unknown-linux-gnu]
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
- name: Install Cargo fuzz
|
|
||||||
run: cargo install cargo-fuzz
|
|
||||||
- name: Fuzz Build
|
|
||||||
run: cargo fuzz build
|
|
||||||
- name: Fuzz Check
|
|
||||||
run: cargo fuzz check
|
|
||||||
openapi:
|
|
||||||
name: openapi
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.openapi == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: openapitools/openapi-generator-cli
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Validate OpenAPI
|
|
||||||
run: |
|
|
||||||
/usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml
|
|
||||||
typos:
|
|
||||||
name: typos
|
|
||||||
needs: [preflight]
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1
|
|
||||||
quality:
|
|
||||||
name: quality
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Beta clippy is non-blocking; continue-on-error below keeps the
|
|
||||||
# aggregated needs.quality.result green when only beta fails.
|
|
||||||
continue-on-error: ${{ matrix.experimental }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
rust:
|
|
||||||
- beta
|
|
||||||
- stable
|
|
||||||
target:
|
|
||||||
- aarch64-unknown-linux-gnu
|
|
||||||
- aarch64-unknown-linux-musl
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
include:
|
|
||||||
- rust: beta
|
|
||||||
experimental: true
|
|
||||||
- rust: stable
|
|
||||||
experimental: false
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
override: true
|
|
||||||
components: clippy
|
|
||||||
- name: Bisectability Check (default features)
|
|
||||||
if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }})
|
|
||||||
for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done
|
|
||||||
git checkout ${{ github.sha }}
|
|
||||||
- name: Clippy (kvm)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm" -- -D warnings
|
|
||||||
- name: Clippy (mshv)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings
|
|
||||||
- name: Clippy (mshv + kvm)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,kvm" -- -D warnings
|
|
||||||
- name: Clippy (default features)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples -- -D warnings
|
|
||||||
- name: Clippy (default features + guest_debug)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings
|
|
||||||
- name: Clippy (default features + pvmemcontrol)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples --features "pvmemcontrol" -- -D warnings
|
|
||||||
- name: Clippy (default features + tracing)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings
|
|
||||||
- name: Clippy (default features + fw_cfg)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "fw_cfg" -- -D warnings
|
|
||||||
- name: Clippy (default features + ivshmem)
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings
|
|
||||||
- name: Clippy (sev_snp)
|
|
||||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings
|
|
||||||
- name: Clippy (igvm)
|
|
||||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings
|
|
||||||
- name: Clippy (kvm + tdx)
|
|
||||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings
|
|
||||||
- name: Clippy (kvm + igvm + sev_snp + fw_cfg)
|
|
||||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,igvm,sev_snp,fw_cfg" -- -D warnings
|
|
||||||
- name: Clippy (default features + sev_snp + igvm + fw_cfg)
|
|
||||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
|
||||||
uses: houseabsolute/actions-rust-cross@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
args: --locked --all --all-targets --tests --examples --features "sev_snp,igvm,fw_cfg" -- -D warnings
|
|
||||||
- name: Check build did not modify any files
|
|
||||||
run: test -z "$(git status --porcelain)"
|
|
||||||
build:
|
|
||||||
name: build
|
|
||||||
needs: [preflight]
|
|
||||||
if: needs.preflight.outputs.full == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
- beta
|
|
||||||
- nightly
|
|
||||||
- "1.89.0" # MSRV — keep quoted.
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install musl-gcc
|
|
||||||
run: sudo apt install -y musl-tools
|
|
||||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
- name: Build (default features)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor
|
|
||||||
- name: Build (kvm)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm"
|
|
||||||
- name: Build (default features + tdx)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "tdx"
|
|
||||||
- name: Build (default features + dbus_api)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "dbus_api"
|
|
||||||
- name: Build (default features + guest_debug)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "guest_debug"
|
|
||||||
- name: Build (default features + pvmemcontrol)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol"
|
|
||||||
- name: Build (default features + fw_cfg)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg"
|
|
||||||
- name: Build (default features + ivshmem)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --features "ivshmem"
|
|
||||||
- name: Build (mshv)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv"
|
|
||||||
- name: Build (sev_snp)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "sev_snp"
|
|
||||||
- name: Build (kvm + igvm + sev_snp + fw_cfg)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm,sev_snp,fw_cfg"
|
|
||||||
- name: Build (igvm)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "igvm"
|
|
||||||
- name: Build (mshv + kvm)
|
|
||||||
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm"
|
|
||||||
- name: Release Build (default features)
|
|
||||||
run: cargo build --locked --all --release --target=${{ matrix.target }}
|
|
||||||
- name: Check build did not modify any files
|
|
||||||
run: test -z "$(git status --porcelain)"
|
|
||||||
# garm-jammy + gnu: runs on PR and MQ. Other 3 matrix entries are in
|
|
||||||
# integration-x86-64-mq (sibling, MQ-only, runs in parallel).
|
|
||||||
integration-x86-64-pr:
|
|
||||||
name: integration-x86-64-pr
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
timeout-minutes: 80
|
|
||||||
env:
|
|
||||||
# Our runner has 16 cores (nproc).
|
|
||||||
# We limit parallelism only to avoid exhausting disk space and memory
|
|
||||||
# resources, not to save CPU resources.
|
|
||||||
PARALLEL_INTEGRATION_TESTS_NUM: 12
|
|
||||||
runs-on: garm-jammy-16
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install Docker
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get -y install ca-certificates curl gnupg
|
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt install -y docker-ce docker-ce-cli
|
|
||||||
- name: Prepare for VDPA
|
|
||||||
run: scripts/prepare_vdpa.sh
|
|
||||||
- name: Run unit tests
|
|
||||||
run: scripts/dev_cli.sh tests --unit --libc gnu
|
|
||||||
- name: Load openvswitch module
|
|
||||||
run: sudo modprobe openvswitch
|
|
||||||
- name: Run integration tests
|
|
||||||
timeout-minutes: 60
|
|
||||||
run: scripts/dev_cli.sh tests --integration --libc gnu
|
|
||||||
# MQ-only: the 3 matrix entries that integration-x86-64-pr does not cover.
|
|
||||||
integration-x86-64-mq:
|
|
||||||
name: integration-x86-64-mq
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
timeout-minutes: 80
|
|
||||||
env:
|
|
||||||
# Our runner has 16 cores (nproc).
|
|
||||||
# We limit parallelism only to avoid exhausting disk space and memory
|
|
||||||
# resources, not to save CPU resources.
|
|
||||||
PARALLEL_INTEGRATION_TESTS_NUM: 12
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- {runner: garm-jammy, libc: musl}
|
|
||||||
- {runner: garm-jammy-amd, libc: gnu}
|
|
||||||
- {runner: garm-jammy-amd, libc: musl}
|
|
||||||
# format() because `${{ matrix.runner }}-16` is not valid in runs-on.
|
|
||||||
runs-on: ${{ format('{0}-16', matrix.runner) }}
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install Docker
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get -y install ca-certificates curl gnupg
|
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt install -y docker-ce docker-ce-cli
|
|
||||||
- name: Prepare for VDPA
|
|
||||||
run: scripts/prepare_vdpa.sh
|
|
||||||
- name: Run unit tests
|
|
||||||
run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }}
|
|
||||||
- name: Load openvswitch module
|
|
||||||
run: sudo modprobe openvswitch
|
|
||||||
- name: Run integration tests
|
|
||||||
timeout-minutes: 60
|
|
||||||
run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }}
|
|
||||||
integration-arm64:
|
|
||||||
name: integration-arm64
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
|
||||||
# Our runner has 80 cores (nproc).
|
|
||||||
# We limit parallelism only to avoid exhausting disk space and memory
|
|
||||||
# resources, not to save CPU resources.
|
|
||||||
PARALLEL_INTEGRATION_TESTS_NUM: 25
|
|
||||||
runs-on: bookworm-arm64
|
|
||||||
steps:
|
|
||||||
# arm64 runner user is "runner" (vfio's is "github-runner").
|
|
||||||
- name: Fix workspace permissions
|
|
||||||
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Run unit tests (musl)
|
|
||||||
run: scripts/dev_cli.sh tests --unit --libc musl
|
|
||||||
- name: Load openvswitch module
|
|
||||||
run: sudo modprobe openvswitch
|
|
||||||
- name: Run integration tests (musl)
|
|
||||||
timeout-minutes: 60
|
|
||||||
run: scripts/dev_cli.sh tests --integration --libc musl
|
|
||||||
- name: Install Azure CLI
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
|
|
||||||
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
|
|
||||||
echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y azure-cli
|
|
||||||
- name: Download Windows image
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw
|
|
||||||
IMG_PATH=$HOME/workloads/$IMG_BASENAME
|
|
||||||
IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz
|
|
||||||
IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-9-min.raw.gz
|
|
||||||
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
|
|
||||||
pushd "$HOME/workloads"
|
|
||||||
if sha1sum "$IMG_BASENAME.sha1" --check; then
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
popd
|
|
||||||
mkdir -p "$HOME/workloads"
|
|
||||||
az storage blob download --container-name private-images --file "$IMG_GZ_PATH" --name "$IMG_GZ_BLOB_NAME" --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
|
|
||||||
gzip -d "$IMG_GZ_PATH"
|
|
||||||
- name: Run Windows guest integration tests
|
|
||||||
timeout-minutes: 30
|
|
||||||
run: scripts/dev_cli.sh tests --integration-windows --libc musl
|
|
||||||
integration-vfio:
|
|
||||||
name: integration-vfio
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
runs-on: vfio-nvidia
|
|
||||||
env:
|
|
||||||
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
|
|
||||||
steps:
|
|
||||||
# vfio-nvidia runner user is "github-runner" (not "runner" like arm64).
|
|
||||||
- name: Fix workspace permissions
|
|
||||||
run: sudo chown -R github-runner:github-runner "${GITHUB_WORKSPACE}"
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Run VFIO integration tests
|
|
||||||
timeout-minutes: 25
|
|
||||||
run: scripts/dev_cli.sh tests --integration-vfio
|
|
||||||
# Most tests are failing with musl, see #6790
|
|
||||||
# - name: Run VFIO integration tests for musl
|
|
||||||
# timeout-minutes: 25
|
|
||||||
# run: scripts/dev_cli.sh tests --integration-vfio --libc musl
|
|
||||||
integration-windows:
|
|
||||||
name: integration-windows
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
runs-on: garm-jammy-16
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install Docker
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get -y install ca-certificates curl gnupg
|
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt install -y docker-ce docker-ce-cli
|
|
||||||
- name: Install Azure CLI
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
|
|
||||||
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
|
|
||||||
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y azure-cli
|
|
||||||
- name: Download Windows image
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
mkdir $HOME/workloads
|
|
||||||
az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2025-amd64-1.raw" --name windows-server-2025-amd64-1.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
|
|
||||||
- name: Run Windows guest integration tests
|
|
||||||
timeout-minutes: 15
|
|
||||||
run: scripts/dev_cli.sh tests --integration-windows
|
|
||||||
- name: Run Windows guest integration tests for musl
|
|
||||||
timeout-minutes: 15
|
|
||||||
run: scripts/dev_cli.sh tests --integration-windows --libc musl
|
|
||||||
integration-rate-limiter:
|
|
||||||
name: integration-rate-limiter
|
|
||||||
needs: [preflight, dco, quality, build]
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
|
|
||||||
runs-on: bare-metal-9950x
|
|
||||||
env:
|
|
||||||
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- name: Code checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Run rate-limiter integration tests
|
|
||||||
timeout-minutes: 20
|
|
||||||
run: scripts/dev_cli.sh tests --integration-rate-limiter
|
|
||||||
# The single required-status check. Branch protection requires this one job.
|
|
||||||
all-green:
|
|
||||||
name: all-green
|
|
||||||
needs:
|
|
||||||
- audit
|
|
||||||
- build
|
|
||||||
- dco
|
|
||||||
- formatting
|
|
||||||
- fuzz-build
|
|
||||||
- gitlint
|
|
||||||
- hadolint
|
|
||||||
- integration-arm64
|
|
||||||
# VFIO worker is failing #8160
|
|
||||||
# - integration-vfio
|
|
||||||
# See: #8211
|
|
||||||
# - integration-windows
|
|
||||||
- integration-x86-64-mq
|
|
||||||
- integration-x86-64-pr
|
|
||||||
- openapi
|
|
||||||
- package-consistency
|
|
||||||
- preflight
|
|
||||||
- quality
|
|
||||||
- reuse
|
|
||||||
- shlint
|
|
||||||
- taplo
|
|
||||||
- typos
|
|
||||||
if: always()
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Verify all dependencies succeeded or were skipped
|
|
||||||
env:
|
|
||||||
NEEDS_JSON: ${{ toJson(needs) }}
|
|
||||||
run: |
|
|
||||||
set -eufo pipefail
|
|
||||||
echo "$NEEDS_JSON" | jq .
|
|
||||||
# success or skipped = pass; failure or cancelled = red.
|
|
||||||
echo "$NEEDS_JSON" | jq -e '
|
|
||||||
to_entries
|
|
||||||
| map(select(.value.result != "success" and .value.result != "skipped"))
|
|
||||||
| length == 0
|
|
||||||
' >/dev/null
|
|
||||||
20
.github/workflows/dco.yaml
vendored
Normal file
20
.github/workflows/dco.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: DCO
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: DCO Check ("Signed-off-by")
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up Python 3.x
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Check DCO
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
pip3 install -U dco-check
|
||||||
|
dco-check -e "49699333+dependabot[bot]@users.noreply.github.com"
|
||||||
14
.github/workflows/docker-image.yaml
vendored
14
.github/workflows/docker-image.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths: resources/Dockerfile
|
paths: resources/Dockerfile
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -21,13 +21,13 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to ghcr
|
- name: Login to ghcr
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
# generate Docker tags based on the following events/attributes
|
# generate Docker tags based on the following events/attributes
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: ./resources/Dockerfile
|
file: ./resources/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build only
|
- name: Build only
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: ./resources/Dockerfile
|
file: ./resources/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
32
.github/workflows/formatting.yaml
vendored
Normal file
32
.github/workflows/formatting.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Cloud Hypervisor Code Formatting
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Code Formatting
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- nightly
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- aarch64-unknown-linux-musl
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
components: rustfmt
|
||||||
|
- name: Formatting (rustfmt)
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
- name: Formatting (fuzz) (rustfmt)
|
||||||
|
run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check
|
||||||
32
.github/workflows/fuzz-build.yaml
vendored
Normal file
32
.github/workflows/fuzz-build.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Cloud Hypervisor Cargo Fuzz Build
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Cargo Fuzz Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- nightly
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
- name: Install Cargo fuzz
|
||||||
|
run: cargo install cargo-fuzz
|
||||||
|
- name: Fuzz Build
|
||||||
|
run: cargo fuzz build
|
||||||
|
- name: Fuzz Check
|
||||||
|
run: cargo fuzz check
|
||||||
25
.github/workflows/gitlint.yaml
vendored
Normal file
25
.github/workflows/gitlint.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Commit messages check
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitlint:
|
||||||
|
name: Check commit messages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Python 3.10
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install --upgrade gitlint
|
||||||
|
- name: Lint git commit messages
|
||||||
|
run: |
|
||||||
|
gitlint --commits origin/$GITHUB_BASE_REF..
|
||||||
25
.github/workflows/hadolint.yaml
vendored
Normal file
25
.github/workflows/hadolint.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Lint Dockerfile
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- resources/Dockerfile
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- resources/Dockerfile
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
hadolint:
|
||||||
|
name: Run Hadolint Dockerfile Linter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Lint Dockerfile
|
||||||
|
uses: hadolint/hadolint-action@master
|
||||||
|
with:
|
||||||
|
dockerfile: ./resources/Dockerfile
|
||||||
|
format: tty
|
||||||
|
no-fail: false
|
||||||
|
verbose: true
|
||||||
|
failure-threshold: info
|
||||||
54
.github/workflows/integration-arm64.yaml
vendored
Normal file
54
.github/workflows/integration-arm64.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Cloud Hypervisor Tests (ARM64)
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 120
|
||||||
|
name: Tests (ARM64)
|
||||||
|
runs-on: bookworm-arm64
|
||||||
|
steps:
|
||||||
|
- name: Fix workspace permissions
|
||||||
|
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Run unit tests (musl)
|
||||||
|
run: scripts/dev_cli.sh tests --unit --libc musl
|
||||||
|
- name: Load openvswitch module
|
||||||
|
run: sudo modprobe openvswitch
|
||||||
|
- name: Run integration tests (musl)
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: scripts/dev_cli.sh tests --integration --libc musl
|
||||||
|
- name: Install Azure CLI
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
|
||||||
|
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
|
||||||
|
echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y azure-cli
|
||||||
|
- name: Download Windows image
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw
|
||||||
|
IMG_PATH=$HOME/workloads/$IMG_BASENAME
|
||||||
|
IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz
|
||||||
|
IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-9-min.raw.gz
|
||||||
|
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
|
||||||
|
pushd "$HOME/workloads"
|
||||||
|
if sha1sum "$IMG_BASENAME.sha1" --check; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
popd
|
||||||
|
mkdir -p "$HOME/workloads"
|
||||||
|
az storage blob download --container-name private-images --file "$IMG_GZ_PATH" --name "$IMG_GZ_BLOB_NAME" --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
|
||||||
|
gzip -d $IMG_GZ_PATH
|
||||||
|
- name: Run Windows guest integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: scripts/dev_cli.sh tests --integration-windows --libc musl
|
||||||
2
.github/workflows/integration-metrics.yaml
vendored
2
.github/workflows/integration-metrics.yaml
vendored
@@ -17,6 +17,6 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Run metrics tests
|
- name: Run metrics tests
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run: scripts/dev_cli.sh tests --metrics -- --test-exclude micro_ -- --report-file /root/workloads/metrics.json
|
run: scripts/dev_cli.sh tests --metrics -- -- --report-file /root/workloads/metrics.json
|
||||||
- name: Upload metrics report
|
- name: Upload metrics report
|
||||||
run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json'
|
run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json'
|
||||||
|
|||||||
25
.github/workflows/integration-rate-limiter.yaml
vendored
Normal file
25
.github/workflows/integration-rate-limiter.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Cloud Hypervisor Tests (Rate-Limiter)
|
||||||
|
on: [merge_group, pull_request]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Tests (Rate-Limiter)
|
||||||
|
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'bare-metal-9950x' }}
|
||||||
|
env:
|
||||||
|
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Run rate-limiter integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
timeout-minutes: 20
|
||||||
|
run: scripts/dev_cli.sh tests --integration-rate-limiter
|
||||||
|
- name: Skipping build for PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
run: echo "Skipping build for PR"
|
||||||
33
.github/workflows/integration-vfio.yaml
vendored
Normal file
33
.github/workflows/integration-vfio.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Cloud Hypervisor Tests (VFIO)
|
||||||
|
on: [merge_group, pull_request]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Tests (VFIO)
|
||||||
|
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'vfio-nvidia' }}
|
||||||
|
env:
|
||||||
|
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Fix workspace permissions
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
|
||||||
|
- name: Code checkout
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Run VFIO integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
run: scripts/dev_cli.sh tests --integration-vfio
|
||||||
|
# Most tests are failing with musl see #6790
|
||||||
|
# - name: Run VFIO integration tests for musl
|
||||||
|
# if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
# timeout-minutes: 15
|
||||||
|
# run: scripts/dev_cli.sh tests --integration-vfio --libc musl
|
||||||
|
- name: Skipping build for PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
run: echo "Skipping build for PR"
|
||||||
50
.github/workflows/integration-windows.yaml
vendored
Normal file
50
.github/workflows/integration-windows.yaml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Cloud Hypervisor Tests (Windows Guest)
|
||||||
|
on: [merge_group, pull_request]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Tests (Windows Guest)
|
||||||
|
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'garm-jammy-16' }}
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install Docker
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install ca-certificates curl gnupg
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli
|
||||||
|
- name: Install Azure CLI
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
|
||||||
|
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
|
||||||
|
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y azure-cli
|
||||||
|
- name: Download Windows image
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
mkdir $HOME/workloads
|
||||||
|
az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2022-amd64-2.raw" --name windows-server-2022-amd64-2.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
|
||||||
|
- name: Run Windows guest integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
run: scripts/dev_cli.sh tests --integration-windows
|
||||||
|
- name: Run Windows guest integration tests for musl
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
run: scripts/dev_cli.sh tests --integration-windows --libc musl
|
||||||
|
- name: Skipping build for PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
run: echo "Skipping build for PR"
|
||||||
52
.github/workflows/integration-x86-64.yaml
vendored
Normal file
52
.github/workflows/integration-x86-64.yaml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Cloud Hypervisor Tests (x86-64)
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 60
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
runner: ['garm-jammy', "garm-jammy-amd"]
|
||||||
|
libc: ["musl", 'gnu']
|
||||||
|
name: Tests (x86-64)
|
||||||
|
runs-on: ${{ github.event_name == 'pull_request' && !(matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') && 'ubuntu-latest' || format('{0}-16', matrix.runner) }}
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install Docker
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install ca-certificates curl gnupg
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli
|
||||||
|
- name: Prepare for VDPA
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
run: scripts/prepare_vdpa.sh
|
||||||
|
- name: Run unit tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }}
|
||||||
|
- name: Load openvswitch module
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
run: sudo modprobe openvswitch
|
||||||
|
- name: Run integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
timeout-minutes: 40
|
||||||
|
run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }}
|
||||||
|
- name: Run live-migration integration tests
|
||||||
|
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
|
||||||
|
timeout-minutes: 20
|
||||||
|
run: scripts/dev_cli.sh tests --integration-live-migration --libc ${{ matrix.libc }}
|
||||||
|
- name: Skipping build for PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' && matrix.runner != 'garm-jammy' && matrix.libc != 'gnu' }}
|
||||||
|
run: echo "Skipping build for PR"
|
||||||
45
.github/workflows/lychee.yaml
vendored
Normal file
45
.github/workflows/lychee.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Link Check (lychee)
|
||||||
|
on: pull_request
|
||||||
|
jobs:
|
||||||
|
link_check:
|
||||||
|
name: Link Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# Fetch the entire history so git diff can compare against the base branch
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Get changed files in PR
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v47 # Using a dedicated action for robustness
|
||||||
|
with:
|
||||||
|
# Compare the HEAD of the PR with the merge-base (where the PR branches off)
|
||||||
|
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
|
# NEW STEP: Print all changed-files outputs for verification
|
||||||
|
- name: Verify Changed Files
|
||||||
|
run: |
|
||||||
|
echo "--- tj-actions/changed-files Outputs ---"
|
||||||
|
echo "any_changed: ${{ steps.changed-files.outputs.any_changed }}"
|
||||||
|
echo "all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}"
|
||||||
|
echo "added_files: ${{ steps.changed-files.outputs.added_files }}"
|
||||||
|
echo "modified_files: ${{ steps.changed-files.outputs.modified_files }}"
|
||||||
|
echo "deleted_files: ${{ steps.changed-files.outputs.deleted_files }}"
|
||||||
|
echo "renamed_files: ${{ steps.changed-files.outputs.renamed_files }}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
# This will also show if the all_changed_files string is empty or not
|
||||||
|
if [ -n "${{ steps.changed-files.outputs.all_changed_files }}" ]; then
|
||||||
|
echo "Detected changes: all_changed_files output is NOT empty."
|
||||||
|
else
|
||||||
|
echo "No changes detected: all_changed_files output IS empty."
|
||||||
|
fi
|
||||||
|
- name: Link Availability Check (Diff Only)
|
||||||
|
# MODIFIED: Only run lychee if the 'all_changed_files' output is not an empty string
|
||||||
|
if: ${{ steps.changed-files.outputs.all_changed_files != '' }}
|
||||||
|
uses: lycheeverse/lychee-action@master
|
||||||
|
with:
|
||||||
|
# Pass the space-separated list of changed files to lychee
|
||||||
|
args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }}
|
||||||
|
failIfEmpty: false
|
||||||
|
fail: true
|
||||||
83
.github/workflows/mshv-infra.yaml
vendored
83
.github/workflows/mshv-infra.yaml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: MSHV Infra Setup
|
name: MSHV Infra Setup
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
ARCH:
|
ARCH:
|
||||||
@@ -13,7 +13,7 @@ on:
|
|||||||
OS_DISK_SIZE:
|
OS_DISK_SIZE:
|
||||||
description: 'OS Disk Size in GB'
|
description: 'OS Disk Size in GB'
|
||||||
required: true
|
required: true
|
||||||
type: number
|
type: string
|
||||||
RG:
|
RG:
|
||||||
description: 'Resource Group Name'
|
description: 'Resource Group Name'
|
||||||
required: true
|
required: true
|
||||||
@@ -44,12 +44,13 @@ on:
|
|||||||
description: 'Private IP of the VM'
|
description: 'Private IP of the VM'
|
||||||
value: ${{ jobs.infra-setup.outputs.PRIVATE_IP }}
|
value: ${{ jobs.infra-setup.outputs.PRIVATE_IP }}
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
infra-setup:
|
infra-setup:
|
||||||
name: ${{ inputs.ARCH }} VM Provision
|
name: ${{ inputs.ARCH }} VM Provision
|
||||||
runs-on: mshv
|
runs-on: mshv
|
||||||
|
continue-on-error: true
|
||||||
outputs:
|
outputs:
|
||||||
RG_NAME: ${{ steps.rg-setup.outputs.RG_NAME }}
|
RG_NAME: ${{ steps.rg-setup.outputs.RG_NAME }}
|
||||||
VM_NAME: ${{ steps.vm-setup.outputs.VM_NAME }}
|
VM_NAME: ${{ steps.vm-setup.outputs.VM_NAME }}
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MI_CLIENT_ID: ${{ secrets.MI_CLIENT_ID }}
|
MI_CLIENT_ID: ${{ secrets.MI_CLIENT_ID }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Installing Azure CLI if not already installed"
|
echo "Installing Azure CLI if not already installed"
|
||||||
if ! command -v az &>/dev/null; then
|
if ! command -v az &>/dev/null; then
|
||||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||||
@@ -68,7 +69,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
az --version
|
az --version
|
||||||
echo "Logging into Azure CLI using Managed Identity"
|
echo "Logging into Azure CLI using Managed Identity"
|
||||||
az login --identity --client-id "${MI_CLIENT_ID}"
|
az login --identity --client-id ${MI_CLIENT_ID}
|
||||||
|
|
||||||
- name: Get Location
|
- name: Get Location
|
||||||
id: get-location
|
id: get-location
|
||||||
@@ -76,13 +77,13 @@ jobs:
|
|||||||
SKU: ${{ inputs.VM_SKU }}
|
SKU: ${{ inputs.VM_SKU }}
|
||||||
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
# Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2)
|
# Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2)
|
||||||
if ! [[ "$SKU" =~ ^Standard_[A-Za-z]+([1-9][0-9]*) ]]; then
|
vcpu=$(echo "$SKU" | sed -n 's/^Standard_[A-Za-z]\+\([0-9]\+\).*/\1/p')
|
||||||
printf 'Cannot extract vCPU count from SKU: %q\n' "$SKU"
|
if [[ -z "$vcpu" ]]; then
|
||||||
|
echo "Cannot extract vCPU count from SKU: $SKU"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
vcpu=${BASH_REMATCH[1]}
|
|
||||||
|
|
||||||
SUPPORTED_LOCATIONS=$(echo "$STORAGE_ACCOUNT_PATHS" | jq -r 'to_entries[] | .key')
|
SUPPORTED_LOCATIONS=$(echo "$STORAGE_ACCOUNT_PATHS" | jq -r 'to_entries[] | .key')
|
||||||
|
|
||||||
@@ -93,9 +94,11 @@ jobs:
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
remaining=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json |
|
usage=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json)
|
||||||
jq '(.limit | tonumber) - (.currentValue | tonumber) >= ($ARGS.positional[0] | tonumber)' --jsonargs "$vcpu")
|
current=$(echo "$usage" | jq -r '.currentValue')
|
||||||
if [[ "$remaining" = true ]]; then
|
limit=$(echo "$usage" | jq -r '.limit')
|
||||||
|
|
||||||
|
if [[ $((limit - current)) -ge $vcpu ]]; then
|
||||||
echo "Sufficient quota found in $location"
|
echo "Sufficient quota found in $location"
|
||||||
echo "location=$location" >> "$GITHUB_OUTPUT"
|
echo "location=$location" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -112,11 +115,11 @@ jobs:
|
|||||||
RG: ${{ inputs.RG }}
|
RG: ${{ inputs.RG }}
|
||||||
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Creating Resource Group: $RG"
|
echo "Creating Resource Group: $RG"
|
||||||
# Create the resource group
|
# Create the resource group
|
||||||
echo "Creating resource group in location: ${LOCATION}"
|
echo "Creating resource group in location: ${LOCATION}"
|
||||||
az group create --name "${RG}" --location "${LOCATION}"
|
az group create --name ${RG} --location ${LOCATION}
|
||||||
echo "RG_NAME=${RG}" >> $GITHUB_OUTPUT
|
echo "RG_NAME=${RG}" >> $GITHUB_OUTPUT
|
||||||
echo "Resource group created successfully."
|
echo "Resource group created successfully."
|
||||||
|
|
||||||
@@ -125,10 +128,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
KEY: ${{ inputs.KEY }}
|
KEY: ${{ inputs.KEY }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Generating SSH key: $KEY"
|
echo "Generating SSH key: $KEY"
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
ssh-keygen -t rsa -b 4096 -f ~/.ssh/"${KEY}" -N ""
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/${KEY} -N ""
|
||||||
|
|
||||||
- name: Create VM
|
- name: Create VM
|
||||||
id: vm-setup
|
id: vm-setup
|
||||||
@@ -143,12 +146,12 @@ jobs:
|
|||||||
VM_IMAGE_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_image
|
VM_IMAGE_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_image
|
||||||
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Creating $VM_SKU VM: $VM_NAME"
|
echo "Creating $VM_SKU VM: $VM_NAME"
|
||||||
|
|
||||||
# Extract subnet ID from the runner VM
|
# Extract subnet ID from the runner VM
|
||||||
echo "Retrieving subnet ID..."
|
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")
|
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
|
if [[ -z "${SUBNET_ID}" ]]; then
|
||||||
echo "ERROR: Failed to retrieve Subnet ID."
|
echo "ERROR: Failed to retrieve Subnet ID."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -156,7 +159,7 @@ jobs:
|
|||||||
|
|
||||||
# Extract image ID from the runner VM
|
# Extract image ID from the runner VM
|
||||||
echo "Retrieving image ID..."
|
echo "Retrieving image ID..."
|
||||||
IMAGE_ID=$(az image show --resource-group "$RUNNER_RG" --name "$VM_IMAGE_NAME" --query "id" -o tsv)
|
IMAGE_ID=$(az image show --resource-group ${RUNNER_RG} --name ${VM_IMAGE_NAME} --query "id" -o tsv)
|
||||||
if [[ -z "${IMAGE_ID}" ]]; then
|
if [[ -z "${IMAGE_ID}" ]]; then
|
||||||
echo "ERROR: Failed to retrieve Image ID."
|
echo "ERROR: Failed to retrieve Image ID."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -164,24 +167,24 @@ jobs:
|
|||||||
|
|
||||||
# Create VM
|
# Create VM
|
||||||
az vm create \
|
az vm create \
|
||||||
--resource-group "${RG}" \
|
--resource-group ${RG} \
|
||||||
--name "${VM_NAME}" \
|
--name ${VM_NAME} \
|
||||||
--subnet "${SUBNET_ID}" \
|
--subnet ${SUBNET_ID} \
|
||||||
--size "${VM_SKU}" \
|
--size ${VM_SKU} \
|
||||||
--location "${LOCATION}" \
|
--location ${LOCATION} \
|
||||||
--image "${IMAGE_ID}" \
|
--image ${IMAGE_ID} \
|
||||||
--os-disk-size-gb "${OS_DISK_SIZE}" \
|
--os-disk-size-gb ${OS_DISK_SIZE} \
|
||||||
--public-ip-sku Standard \
|
--public-ip-sku Standard \
|
||||||
--storage-sku Premium_LRS \
|
--storage-sku Premium_LRS \
|
||||||
--public-ip-address "" \
|
--public-ip-address "" \
|
||||||
--admin-username "${USERNAME}" \
|
--admin-username ${USERNAME} \
|
||||||
--ssh-key-value ~/.ssh/"${KEY}".pub \
|
--ssh-key-value ~/.ssh/${KEY}.pub \
|
||||||
--security-type Standard \
|
--security-type Standard \
|
||||||
--output json
|
--output json
|
||||||
|
|
||||||
az vm boot-diagnostics enable --name "${VM_NAME}" --resource-group "${RG}"
|
az vm boot-diagnostics enable --name ${VM_NAME} --resource-group ${RG}
|
||||||
|
|
||||||
echo "VM_NAME=${VM_NAME}" >> "$GITHUB_OUTPUT"
|
echo "VM_NAME=${VM_NAME}" >> $GITHUB_OUTPUT
|
||||||
echo "VM creation process completed successfully."
|
echo "VM creation process completed successfully."
|
||||||
|
|
||||||
- name: Get VM Private IP
|
- name: Get VM Private IP
|
||||||
@@ -190,15 +193,15 @@ jobs:
|
|||||||
RG: ${{ inputs.RG }}
|
RG: ${{ inputs.RG }}
|
||||||
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Retrieving VM Private IP address..."
|
echo "Retrieving VM Private IP address..."
|
||||||
# Retrieve VM Private IP address
|
# Retrieve VM Private IP address
|
||||||
PRIVATE_IP=$(az vm show -g "${RG}" -n "${VM_NAME}" -d --query privateIps -o tsv)
|
PRIVATE_IP=$(az vm show -g ${RG} -n ${VM_NAME} -d --query privateIps -o tsv)
|
||||||
if [[ -z "$PRIVATE_IP" ]]; then
|
if [[ -z "$PRIVATE_IP" ]]; then
|
||||||
echo "ERROR: Failed to retrieve private IP address."
|
echo "ERROR: Failed to retrieve private IP address."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "PRIVATE_IP=$PRIVATE_IP" >> "$GITHUB_OUTPUT"
|
echo "PRIVATE_IP=$PRIVATE_IP" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Wait for SSH availability
|
- name: Wait for SSH availability
|
||||||
env:
|
env:
|
||||||
@@ -207,16 +210,16 @@ jobs:
|
|||||||
USERNAME: ${{ secrets.USERNAME }}
|
USERNAME: ${{ secrets.USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
echo "Waiting for SSH to be accessible..."
|
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'
|
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!"
|
echo "VM is accessible!"
|
||||||
|
|
||||||
- name: Remove Old Host Key
|
- name: Remove Old Host Key
|
||||||
env:
|
env:
|
||||||
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Removing the old host key"
|
echo "Removing the old host key"
|
||||||
ssh-keygen -R "$PRIVATE_IP"
|
ssh-keygen -R $PRIVATE_IP
|
||||||
|
|
||||||
- name: SSH into VM and Install Dependencies
|
- name: SSH into VM and Install Dependencies
|
||||||
env:
|
env:
|
||||||
@@ -224,9 +227,9 @@ jobs:
|
|||||||
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
||||||
USERNAME: ${{ secrets.USERNAME }}
|
USERNAME: ${{ secrets.USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" << EOF
|
ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Logged in successfully."
|
echo "Logged in successfully."
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
sudo tdnf install -y git moby-engine moby-cli clang llvm pkg-config make gcc glibc-devel
|
sudo tdnf install -y git moby-engine moby-cli clang llvm pkg-config make gcc glibc-devel
|
||||||
@@ -241,6 +244,6 @@ jobs:
|
|||||||
sudo systemctl enable containerd.service
|
sudo systemctl enable containerd.service
|
||||||
sudo systemctl start docker
|
sudo systemctl start docker
|
||||||
sudo groupadd -f docker
|
sudo groupadd -f docker
|
||||||
sudo usermod -a -G docker "${USERNAME}"
|
sudo usermod -a -G docker ${USERNAME}
|
||||||
sudo systemctl restart docker
|
sudo systemctl restart docker
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
30
.github/workflows/mshv-integration.yaml
vendored
30
.github/workflows/mshv-integration.yaml
vendored
@@ -1,6 +1,5 @@
|
|||||||
name: Cloud Hypervisor Tests (MSHV) (x86_64)
|
name: Cloud Hypervisor Tests (MSHV) (x86_64)
|
||||||
on: [pull_request_target, merge_group]
|
on: [pull_request_target, merge_group]
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
infra-setup:
|
infra-setup:
|
||||||
@@ -24,6 +23,7 @@ jobs:
|
|||||||
needs: infra-setup
|
needs: infra-setup
|
||||||
if: ${{ always() && needs.infra-setup.result == 'success' }}
|
if: ${{ always() && needs.infra-setup.result == 'success' }}
|
||||||
runs-on: mshv
|
runs-on: mshv
|
||||||
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
@@ -36,9 +36,9 @@ jobs:
|
|||||||
RG: MSHV-${{ github.run_id }}
|
RG: MSHV-${{ github.run_id }}
|
||||||
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
echo "Connecting to the VM via SSH..."
|
echo "Connecting to the VM via SSH..."
|
||||||
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" << EOF
|
ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF
|
||||||
set -e
|
set -e
|
||||||
echo "Logged in successfully."
|
echo "Logged in successfully."
|
||||||
export PATH="\$HOME/.cargo/bin:\$PATH"
|
export PATH="\$HOME/.cargo/bin:\$PATH"
|
||||||
@@ -67,12 +67,12 @@ jobs:
|
|||||||
|
|
||||||
echo "Setting permissions..."
|
echo "Setting permissions..."
|
||||||
for i in 0 1 2; do
|
for i in 0 1 2; do
|
||||||
dev="/dev/vhost-vdpa-\$i"
|
dev="/dev/vhost-vdpa-$i"
|
||||||
if [ -e "\$dev" ]; then
|
if [ -e "$dev" ]; then
|
||||||
sudo chown \$USER:\$USER "\$dev"
|
sudo chown $USER:$USER "$dev"
|
||||||
sudo chmod 660 "\$dev"
|
sudo chmod 660 "$dev"
|
||||||
else
|
else
|
||||||
echo "Warning: Device \$dev not found"
|
echo "Warning: Device $dev not found"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -87,7 +87,9 @@ jobs:
|
|||||||
PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }}
|
PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }}
|
||||||
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" sudo dmesg
|
ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF
|
||||||
|
sudo dmesg
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Dump serial console logs
|
- name: Dump serial console logs
|
||||||
if: always()
|
if: always()
|
||||||
@@ -96,7 +98,7 @@ jobs:
|
|||||||
RG_NAME: ${{ needs.infra-setup.outputs.RG_NAME }}
|
RG_NAME: ${{ needs.infra-setup.outputs.RG_NAME }}
|
||||||
VM_NAME: ${{ needs.infra-setup.outputs.VM_NAME }}
|
VM_NAME: ${{ needs.infra-setup.outputs.VM_NAME }}
|
||||||
run: |
|
run: |
|
||||||
set -eufo pipefail
|
set -e
|
||||||
az vm boot-diagnostics get-boot-log --name "${VM_NAME}" --resource-group "${RG_NAME}" | jq -r
|
az vm boot-diagnostics get-boot-log --name "${VM_NAME}" --resource-group "${RG_NAME}" | jq -r
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
@@ -109,8 +111,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RG: MSHV-INTEGRATION-${{ github.run_id }}
|
RG: MSHV-INTEGRATION-${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
if az group exists --name "${RG}"; then
|
if az group exists --name ${RG}; then
|
||||||
az group delete --name "${RG}" --yes --no-wait
|
az group delete --name ${RG} --yes --no-wait
|
||||||
else
|
else
|
||||||
echo "Resource Group ${RG} does not exist. Skipping deletion."
|
echo "Resource Group ${RG} does not exist. Skipping deletion."
|
||||||
fi
|
fi
|
||||||
@@ -120,8 +122,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
KEY: azure_key_${{ github.run_id }}
|
KEY: azure_key_${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
if [ -f ~/.ssh/"${KEY}" ]; then
|
if [ -f ~/.ssh/${KEY} ]; then
|
||||||
rm -f ~/.ssh/"${KEY}" ~/.ssh/"${KEY}.pub"
|
rm -f ~/.ssh/${KEY} ~/.ssh/${KEY}.pub
|
||||||
echo "SSH key deleted successfully."
|
echo "SSH key deleted successfully."
|
||||||
else
|
else
|
||||||
echo "SSH key does not exist. Skipping deletion."
|
echo "SSH key does not exist. Skipping deletion."
|
||||||
|
|||||||
14
.github/workflows/openapi.yaml
vendored
Normal file
14
.github/workflows/openapi.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Cloud Hypervisor OpenAPI Validation
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: openapitools/openapi-generator-cli
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Validate OpenAPI
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
/usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml
|
||||||
32
.github/workflows/package-consistency.yaml
vendored
Normal file
32
.github/workflows/package-consistency.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Cloud Hypervisor Consistency
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Rust VMM Consistency Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt install -y python3
|
||||||
|
|
||||||
|
- name: Install Rust toolchain stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Check Rust VMM Package Consistency of root Workspace
|
||||||
|
run: python3 scripts/package-consistency-check.py github.com/rust-vmm
|
||||||
|
|
||||||
|
- name: Check Rust VMM Package Consistency of fuzz Workspace
|
||||||
|
run: |
|
||||||
|
pushd fuzz
|
||||||
|
python3 ../scripts/package-consistency-check.py github.com/rust-vmm
|
||||||
|
popd
|
||||||
30
.github/workflows/preview-riscv64-build.yaml
vendored
Normal file
30
.github/workflows/preview-riscv64-build.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Cloud Hypervisor RISC-V 64-bit kvm build Preview
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Cargo
|
||||||
|
runs-on: riscv64-qemu-host
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh rustup default 1.89.0
|
||||||
|
|
||||||
|
- name: Build test (kvm)
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh cargo build --locked --no-default-features --features "kvm" -p cloud-hypervisor
|
||||||
|
|
||||||
|
- name: Clippy test (kvm)
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh cargo clippy --locked --no-default-features --features "kvm" -p cloud-hypervisor
|
||||||
|
|
||||||
|
- name: Check no files were modified
|
||||||
|
run: test -z "$(git status --porcelain)"
|
||||||
39
.github/workflows/preview-riscv64-modules.yaml
vendored
Normal file
39
.github/workflows/preview-riscv64-modules.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Cloud Hypervisor RISC-V 64-bit Preview
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Cargo
|
||||||
|
runs-on: riscv64-qemu-host
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
module:
|
||||||
|
- hypervisor
|
||||||
|
- arch
|
||||||
|
- vm-allocator
|
||||||
|
- devices
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh rustup default 1.89.0
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.module }} Module (kvm)
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh cargo build --locked -p ${{ matrix.module }} --no-default-features --features "kvm"
|
||||||
|
|
||||||
|
- name: Clippy ${{ matrix.module }} Module (kvm)
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh cargo clippy --locked -p ${{ matrix.module }} --no-default-features --features "kvm" -- -D warnings
|
||||||
|
|
||||||
|
- name: Test ${{ matrix.module }} Module (kvm)
|
||||||
|
run: /opt/scripts/exec-in-qemu.sh cargo test --locked -p ${{ matrix.module }} --no-default-features --features "kvm"
|
||||||
|
|
||||||
|
- name: Check no files were modified
|
||||||
|
run: test -z "$(git status --porcelain)"
|
||||||
170
.github/workflows/quality.yaml
vendored
Normal file
170
.github/workflows/quality.yaml
vendored
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
name: Cloud Hypervisor Quality Checks
|
||||||
|
on: [pull_request, merge_group]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Quality (clippy)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: ${{ matrix.experimental }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- beta
|
||||||
|
- stable
|
||||||
|
target:
|
||||||
|
- aarch64-unknown-linux-gnu
|
||||||
|
- aarch64-unknown-linux-musl
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
include:
|
||||||
|
- rust: beta
|
||||||
|
experimental: true
|
||||||
|
- rust: stable
|
||||||
|
experimental: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
override: true
|
||||||
|
components: clippy
|
||||||
|
|
||||||
|
- name: Bisectability Check (default features)
|
||||||
|
if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }})
|
||||||
|
for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Clippy (kvm)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (mshv)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (mshv + kvm)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,kvm" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (default features)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --tests --examples -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (default features + guest_debug)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (default features + pvmemcontrol)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --tests --examples --features "pvmemcontrol" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (default features + tracing)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings
|
||||||
|
- name: Clippy (default features + fw_cfg)
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||||
|
command: clippy
|
||||||
|
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "fw_cfg" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (default features + ivshmem)
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (sev_snp)
|
||||||
|
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (igvm)
|
||||||
|
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy (kvm + tdx)
|
||||||
|
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||||
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings
|
||||||
|
|
||||||
|
- name: Check build did not modify any files
|
||||||
|
run: test -z "$(git status --porcelain)"
|
||||||
|
|
||||||
|
typos:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
name: Typos / Spellcheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
# Executes "typos ."
|
||||||
|
- uses: crate-ci/typos@v1.40.0
|
||||||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Cloud Hypervisor Release
|
name: Cloud Hypervisor Release
|
||||||
on: [create, merge_group]
|
on: [create, merge_group]
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }}
|
cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }}
|
||||||
- name: Upload Release Artifacts
|
- name: Upload Release Artifacts
|
||||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Artifacts for ${{ matrix.platform.target }}
|
name: Artifacts for ${{ matrix.platform.target }}
|
||||||
path: |
|
path: |
|
||||||
@@ -80,13 +80,13 @@ jobs:
|
|||||||
github.event_name == 'create' && github.event.ref_type == 'tag' &&
|
github.event_name == 'create' && github.event.ref_type == 'tag' &&
|
||||||
matrix.platform.target == 'x86_64-unknown-linux-gnu'
|
matrix.platform.target == 'x86_64-unknown-linux-gnu'
|
||||||
id: upload-release-cloud-hypervisor-vendored-sources
|
id: upload-release-cloud-hypervisor-vendored-sources
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
path: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
||||||
name: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
name: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
12
.github/workflows/reuse.yaml
vendored
Normal file
12
.github/workflows/reuse.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: REUSE Compliance Check
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
reuse:
|
||||||
|
name: REUSE Compliance Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: REUSE Compliance Check
|
||||||
|
uses: fsfe/reuse-action@v6
|
||||||
20
.github/workflows/shlint.yaml
vendored
Normal file
20
.github/workflows/shlint.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Shell scripts check
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
merge_group:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sh-checker:
|
||||||
|
name: Check shell scripts
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Run the shell script checkers
|
||||||
|
uses: luizm/action-sh-checker@master
|
||||||
|
env:
|
||||||
|
SHFMT_OPTS: -i 4 -d
|
||||||
|
SHELLCHECK_OPTS: -x --source-path scripts
|
||||||
21
.github/workflows/taplo.yaml
vendored
Normal file
21
.github/workflows/taplo.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Cargo.toml Formatting (taplo)
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo_toml_format:
|
||||||
|
name: Cargo.toml Formatting
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev
|
||||||
|
- name: Install taplo
|
||||||
|
run: cargo install taplo-cli --locked
|
||||||
|
- name: Check formatting
|
||||||
|
run: taplo fmt --check
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,13 +1,10 @@
|
|||||||
|
/build
|
||||||
|
/.cargo
|
||||||
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
**/Cargo.lock
|
**/Cargo.lock
|
||||||
**/rusty-tags.vi
|
**/rusty-tags.vi
|
||||||
/.agents
|
|
||||||
/.cargo
|
|
||||||
/.claude
|
|
||||||
/.codex
|
|
||||||
/.vscode
|
|
||||||
/build
|
|
||||||
/rpm/SOURCES
|
/rpm/SOURCES
|
||||||
/target
|
/.vscode
|
||||||
/vendor
|
/vendor
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|||||||
38
.lychee.toml
38
.lychee.toml
@@ -1,33 +1,27 @@
|
|||||||
verbose = "info"
|
verbose = "info"
|
||||||
|
|
||||||
exclude_path = [".lychee.toml"]
|
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
# Availability of links below should be manually verified.
|
# Availability of links below should be manually verified.
|
||||||
# Page for intel TDX support, returns 403 while querying.
|
# Page for intel TDX support, returns 403 while querying.
|
||||||
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
|
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
|
||||||
# Page for TPM, returns 403 while querying.
|
# Page for TPM, returns 403 while querying.
|
||||||
'^https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf',
|
'^https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf',
|
||||||
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
|
|
||||||
'^https://github.com/smibarber',
|
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
|
||||||
# OSDev has added bot protection and accesses my result in 403 Forbidden.
|
'^https://github.com/smibarber',
|
||||||
'^https://wiki.osdev.org',
|
|
||||||
# Exclude all pages with $ in the URL since $XXX is a variable
|
# OSDev has added bot protection and accesses my result in 403 Forbidden.
|
||||||
"\\$.*",
|
'^https://wiki.osdev.org',
|
||||||
# Exclude local files
|
# Exclude all pages with $ in the URL since $XXX is a variable
|
||||||
"file://.*",
|
"\\$.*",
|
||||||
# ARM documentation returns 403 Forbidden for automated CI checks.
|
# Exclude local files
|
||||||
'^http://infocenter\.arm\.com',
|
"file://.*",
|
||||||
'^https://developer\.arm\.com',
|
|
||||||
# Ignore internal/unsupported protocols seen in logs
|
|
||||||
'^tcp://192\.168\.1\.10',
|
|
||||||
# Slack invite endpoints reject automated GETs and return 403.
|
|
||||||
'^https://join\.slack\.com/t/',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Exclude loopback addresses
|
# Exclude loopback addresses
|
||||||
exclude_loopback = true
|
exclude_loopback = true
|
||||||
|
|
||||||
|
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
|
|
||||||
retry_wait_time = 5
|
retry_wait_time = 5
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ Files: docs/*.md *.md
|
|||||||
Copyright: 2024
|
Copyright: 2024
|
||||||
License: CC-BY-4.0
|
License: CC-BY-4.0
|
||||||
|
|
||||||
Files: scripts/* test_data/* *.toml .git* .editorconfig fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock
|
Files: scripts/* test_data/* *.toml .git* fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock
|
||||||
Copyright: 2024
|
Copyright: 2024
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
include = ["**/Cargo.toml"]
|
include = ["**/Cargo.toml"]
|
||||||
|
|
||||||
[formatting]
|
[formatting]
|
||||||
indent_string = " " # 2 spaces: keep in sync with .editorconfig
|
|
||||||
reorder_arrays = true
|
reorder_arrays = true
|
||||||
reorder_keys = true
|
reorder_keys = true
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
"hypervisor/src/kvm/x86_64/mod.rs",
|
"hypervisor/src/kvm/x86_64/mod.rs",
|
||||||
"resources/linux-config-*",
|
"resources/linux-config-*",
|
||||||
]
|
]
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
@@ -21,8 +21,5 @@ liness = "liness"
|
|||||||
outout = "outout"
|
outout = "outout"
|
||||||
|
|
||||||
[default.extend-identifiers]
|
[default.extend-identifiers]
|
||||||
consts = "consts"
|
|
||||||
fo = "fo"
|
fo = "fo"
|
||||||
fpr = "fpr"
|
fpr = "fpr"
|
||||||
# Public Linux API
|
|
||||||
msg_controllen = "msg_controllen"
|
|
||||||
|
|||||||
81
AGENTS.md
81
AGENTS.md
@@ -1,81 +0,0 @@
|
|||||||
## For Humans
|
|
||||||
|
|
||||||
This is a compact [AGENTS.md](https://agents.md/) file for Cloud Hypervisor.
|
|
||||||
It is meant to help automated coding agents make useful changes that stay safe,
|
|
||||||
reviewable, and compatible with the project's normal engineering constraints.
|
|
||||||
|
|
||||||
## For LLMs
|
|
||||||
|
|
||||||
### Project Context
|
|
||||||
|
|
||||||
- Start with `README.md` for the project shape and `CONTRIBUTING.md` for the
|
|
||||||
contribution rules, coding style, commit message guidance, and LLM assistance
|
|
||||||
disclosure policy. Following `CONTRIBUTING.md` is crucial!
|
|
||||||
- The main supported architectures are `x86_64` and `aarch64`; the main
|
|
||||||
hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the most regular
|
|
||||||
exercise, but changes must not make the other first-class targets worse.
|
|
||||||
|
|
||||||
### Change Guidelines
|
|
||||||
|
|
||||||
- Prefer correctness, safety, and readability over micro-optimizations. Keep
|
|
||||||
changes small, reviewable, and aligned with the existing crate/module
|
|
||||||
boundaries. Avoid speculative changes and unrelated refactoring.
|
|
||||||
- For API, config, migration, device model, or hypervisor boundary changes,
|
|
||||||
consider the effect on all architectures and all backends. Changes to one
|
|
||||||
backend can be okay if the other backend still functions properly and could
|
|
||||||
be extended or modified later.
|
|
||||||
- Follow Rust best practices and the style already present in the touched code.
|
|
||||||
- Avoid new dependencies unless the benefit is clear and local alternatives are
|
|
||||||
not enough.
|
|
||||||
- Preserve existing behavior unless the requested change explicitly needs a
|
|
||||||
behavior change; refactors must preserve behavior. Call out compatibility or
|
|
||||||
migration implications.
|
|
||||||
- Do not invent APIs, behavior, or requirements. If something is uncertain,
|
|
||||||
state the uncertainty and proceed only with minimal, explicit assumptions.
|
|
||||||
|
|
||||||
### Safety and Domain Notes
|
|
||||||
|
|
||||||
- Prefer safe Rust. If `unsafe` is necessary, keep it narrow, add a `SAFETY:`
|
|
||||||
comment with the invariants, and make sure the surrounding code upholds them.
|
|
||||||
- Assume concurrency matters. Avoid races, unsynchronized shared state, and
|
|
||||||
implicit ordering assumptions; prefer clear ownership and synchronization.
|
|
||||||
- Keep docs and comments short and useful. Document non-trivial invariants at
|
|
||||||
struct definitions and critical state transitions.
|
|
||||||
- Logging should be minimal and high signal. Use `info!` for important normal
|
|
||||||
state changes that matter in production; use `warn!` or `error!` only for
|
|
||||||
abnormal conditions. Keep `debug!` for focused diagnostics.
|
|
||||||
|
|
||||||
### Build and Test Notes
|
|
||||||
|
|
||||||
- Some workspace members require the `kvm` feature to build or test correctly.
|
|
||||||
When a default build failure looks feature-related, retry the narrow command
|
|
||||||
with `--features kvm` before widening the diagnosis.
|
|
||||||
- Prefer narrow crate/test commands while iterating, then broaden verification
|
|
||||||
when the touched surface justifies it.
|
|
||||||
- Formatting currently needs nightly-only rustfmt features; use
|
|
||||||
`cargo +nightly fmt --all`.
|
|
||||||
- Add targeted unit tests for bug fixes and non-trivial logic where practical.
|
|
||||||
Keep test scaffolding minimal and focused.
|
|
||||||
- Integration tests live in `./cloud-hypervisor/tests/` and are normally driven
|
|
||||||
by `./scripts/dev_cli.sh` / `./scripts/run_integration_tests_*.sh`. They need
|
|
||||||
host privileges, workloads, and container setup. To build the integration-test
|
|
||||||
code directly without the infrastructure from `./scripts`, set the Rust cfg
|
|
||||||
`devcli_testenv` or simply build through `clippy` which automatically includes
|
|
||||||
these code paths; otherwise the integration-test code is not included. Do not
|
|
||||||
assume the tests can be run directly in a restricted agent environment; ask
|
|
||||||
the developer to run them when real integration coverage is needed.
|
|
||||||
|
|
||||||
### Commit and Patch Formatting
|
|
||||||
|
|
||||||
- Follow the rules in `CONTRIBUTING.md`, including reviewable commit structure,
|
|
||||||
valid component prefixes, 72-column commit messages, and a `Signed-off-by`
|
|
||||||
trailer.
|
|
||||||
- Lines in a commit message that are allowed to exceed the 72-column limit are
|
|
||||||
specified in `./scripts/gitlint/rules`.
|
|
||||||
- For LLM-assisted changes, follow the disclosure guidance in `CONTRIBUTING.md`:
|
|
||||||
use the project's `Assisted-by:` trailer when disclosure is needed, and do not
|
|
||||||
add `Co-authored-by` or similar trailers unless that policy changes.
|
|
||||||
- Temporary allowances such as `#[allow(unused)]` or ignored tests are only
|
|
||||||
acceptable if resolved within the same commit series or paired with a clear
|
|
||||||
TODO referencing a ticket. Ask the developer if in doubt.
|
|
||||||
|
|
||||||
169
CONTRIBUTING.md
169
CONTRIBUTING.md
@@ -11,36 +11,17 @@ license of those projects.
|
|||||||
New code should be under the [Apache v2
|
New code should be under the [Apache v2
|
||||||
License](https://opensource.org/licenses/Apache-2.0).
|
License](https://opensource.org/licenses/Apache-2.0).
|
||||||
|
|
||||||
## Coding Style & Code Comments
|
## Coding Style
|
||||||
|
|
||||||
We use the [Rust Style] guide and enforce formatting and linting in CI,
|
We follow the [Rust Style](https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src)
|
||||||
including `rustfmt`, `clippy`, and other common Rust quality checks, for every
|
convention and enforce it through the Continuous Integration (CI) process calling into `rustfmt`,
|
||||||
pull request. We adapt to best practices, new lints and new tooling as the
|
`clippy`, and other well-known code quality tool of the ecosystem for each submitted Pull Request (PR).
|
||||||
ecosystem evolves.
|
|
||||||
|
|
||||||
Code should **speak for itself** (for example, by using descriptive identifiers)
|
|
||||||
and be **easy to read and maintain**. Beyond the conventions and tooling
|
|
||||||
described above, contributors have _some_ room to apply their own style and
|
|
||||||
preferred structure. Maintainers may still suggest refactorings where they
|
|
||||||
believe readability, consistency, or maintainability can be improved.
|
|
||||||
|
|
||||||
For new code, add documentation and comments where they **provide additional value**:
|
|
||||||
|
|
||||||
* **Rustdoc** explains the API to its users.
|
|
||||||
* **Inline comments** explain the code the reader, especially *why* it is
|
|
||||||
written that way.
|
|
||||||
* **Commit messages** explain the broader context of a change (for more
|
|
||||||
information on commit messages, see below).
|
|
||||||
|
|
||||||
Comments should be concise and add additional context or information to the code.
|
|
||||||
|
|
||||||
[Rust Style]: https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src
|
|
||||||
|
|
||||||
## Basic Checks
|
## Basic Checks
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# We currently rely on nightly-only formatting features
|
# We currently rely on nightly-only formatting features
|
||||||
cargo +nightly fmt --all
|
cargo +nightly fmt --all
|
||||||
cargo check --all-targets --tests
|
cargo check --all-targets --tests
|
||||||
cargo clippy --all-targets --tests
|
cargo clippy --all-targets --tests
|
||||||
# Please note that this will not execute integration tests.
|
# Please note that this will not execute integration tests.
|
||||||
@@ -55,7 +36,7 @@ gitlint --commits "HEAD~3..HEAD"
|
|||||||
_Caution: These tests are taking a long time to complete (40+ mins) and need special setup._
|
_Caution: These tests are taking a long time to complete (40+ mins) and need special setup._
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash ./scripts/dev_cli.sh tests --integration -- --test-filter '<optionally filter test by name pattern>'
|
bash ./scripts/dev_cli.sh tests --integration -- --test-filter '<optionally filter test by name pattern>'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setup Commit Hook
|
### Setup Commit Hook
|
||||||
@@ -77,65 +58,42 @@ commit you make.
|
|||||||
|
|
||||||
## Certificate of Origin
|
## Certificate of Origin
|
||||||
|
|
||||||
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://www.kernel.org/doc/Documentation/process/submitting-patches.rst)
|
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://web.archive.org/web/20230406041855/https://01.org/community/signed-process)
|
||||||
used by the Linux kernel project.
|
used by the Linux kernel project.
|
||||||
|
|
||||||
## Patch format & Git Commit Hygiene
|
## Patch format
|
||||||
|
|
||||||
_We use **Patch** as synonym for **Commit**._
|
Beside the signed-off-by footer, we expect each patch to comply with the following format:
|
||||||
|
|
||||||
We require patches to:
|
```
|
||||||
|
<component>: Change summary
|
||||||
|
|
||||||
- Have a `Signed-off-by: Name <email>` footer
|
More detailed explanation of your changes: Why and how.
|
||||||
- Follow the pattern: \
|
Wrap it to 72 characters.
|
||||||
```
|
See http://chris.beams.io/posts/git-commit/
|
||||||
<component>: Change summary
|
for some more good pieces of advice.
|
||||||
|
|
||||||
More detailed explanation of your changes: Why and how.
|
Signed-off-by: <contributor@foo.com>
|
||||||
Wrap it to 72 characters.
|
```
|
||||||
See http://chris.beams.io/posts/git-commit/
|
|
||||||
for some more good pieces of advice.
|
|
||||||
|
|
||||||
Signed-off-by: <contributor@foo.com>
|
For example:
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Valid components are listed in `TitleStartsWithComponent.py`. In short, each
|
|
||||||
cargo workspace member is a valid component as well as `build`, `ci`, `docs` and
|
|
||||||
`misc`.
|
|
||||||
|
|
||||||
Example patch:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
vm-virtio: Reset underlying device on driver request
|
vm-virtio: Reset underlying device on driver request
|
||||||
|
|
||||||
If the driver triggers a reset by writing zero into the status register
|
If the driver triggers a reset by writing zero into the status register
|
||||||
then reset the underlying device if supported. A device reset also
|
then reset the underlying device if supported. A device reset also
|
||||||
requires resetting various aspects of the queue.
|
requires resetting various aspects of the queue.
|
||||||
|
|
||||||
In order to be able to do a subsequent reactivate it is required to
|
In order to be able to do a subsequent reactivate it is required to
|
||||||
reclaim certain resources (interrupt and queue EventFDs.) If a device
|
reclaim certain resources (interrupt and queue EventFDs.) If a device
|
||||||
reset is requested by the driver but the underlying device does not
|
reset is requested by the driver but the underlying device does not
|
||||||
support it then generate an error as the driver would not be able to
|
support it then generate an error as the driver would not be able to
|
||||||
configure it anyway.
|
configure it anyway.
|
||||||
|
|
||||||
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
|
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Git Commit History
|
|
||||||
|
|
||||||
We value a clean, **reviewable** commit history. Each commit should represent
|
|
||||||
a self-contained, logical step that guides reviewers clearly from A to B.
|
|
||||||
|
|
||||||
Avoid patterns like `init A -> init B -> fix A` or \
|
|
||||||
`init design A -> revert A -> use design B`. Commits must be independently
|
|
||||||
reviewable - don't leave "fix previous commit" or earlier design attempts in
|
|
||||||
the history.
|
|
||||||
|
|
||||||
Intermediate work-in-progress changes are acceptable only if a subsequent
|
|
||||||
commit in the same series cleans them up (e.g. a temporary `#[allow(unused)]`
|
|
||||||
removed in the next commit).
|
|
||||||
|
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
Cloud Hypervisor uses the “fork-and-pull” development model. Follow these steps if
|
Cloud Hypervisor uses the “fork-and-pull” development model. Follow these steps if
|
||||||
@@ -146,14 +104,10 @@ you want to merge your changes to `cloud-hypervisor`:
|
|||||||
1. Within your fork, create a branch for your contribution.
|
1. Within your fork, create a branch for your contribution.
|
||||||
1. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
|
1. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
|
||||||
against the main branch of the Cloud Hypervisor repository.
|
against the main branch of the Cloud Hypervisor repository.
|
||||||
1. Each commit must comply with the Commit Hygiene guidelines above.
|
1. To update your pull request amend existing commits whenever applicable and
|
||||||
1. A pull request should address a single component or concern to keep review
|
then push the new changes to your pull request branch.
|
||||||
focused and approvals straightforward.
|
|
||||||
1. Once the pull request is approved it can be integrated.
|
1. Once the pull request is approved it can be integrated.
|
||||||
|
|
||||||
Please squash any changes done during review already into the corresponding
|
|
||||||
commits instead of pushing `<component>: addressing review for A`-style commits.
|
|
||||||
|
|
||||||
## Issue tracking
|
## Issue tracking
|
||||||
|
|
||||||
If you have a problem, please let us know. We recommend using
|
If you have a problem, please let us know. We recommend using
|
||||||
@@ -169,83 +123,26 @@ comments or by adding the `Fixes` keyword to your commit message:
|
|||||||
|
|
||||||
```
|
```
|
||||||
serial: Set terminal in raw mode
|
serial: Set terminal in raw mode
|
||||||
|
|
||||||
In order to have proper output from the serial, we need to setup the
|
In order to have proper output from the serial, we need to setup the
|
||||||
terminal in raw mode. When the VM is shutting down, it is also the
|
terminal in raw mode. When the VM is shutting down, it is also the
|
||||||
VMM responsibility to set the terminal back into canonical mode if we
|
VMM responsibility to set the terminal back into canonical mode if we
|
||||||
don't want to get any weird behavior from the shell.
|
don't want to get any weird behavior from the shell.
|
||||||
|
|
||||||
Fixes #88
|
Fixes #88
|
||||||
|
|
||||||
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
|
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, after the corresponding PR is merged, GitHub will automatically close that issue when parsing the
|
Then, after the corresponding PR is merged, GitHub will automatically close that issue when parsing the
|
||||||
[commit message](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
[commit message](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
||||||
|
|
||||||
## AI/LLM Assistance & Generated Code
|
## AI Generated Code
|
||||||
|
|
||||||
We recommend **a careful and conservative approach** to LLM usage, guided by
|
Our policy is to decline any contributions known to contain contents
|
||||||
sound engineering judgment. Please use AI/LLM-assisted tooling thoughtfully and
|
generated or derived from using Large Language Models (LLMs). This
|
||||||
responsibly to ensure efficient use of limited project resources, particularly
|
includes ChatGPT, Gemini, Claude, Copilot and similar tools.
|
||||||
in code review and long-term maintenance. Our primary goals are to avoid
|
|
||||||
ambiguity in license compliance and to keep contributions clear and easy to
|
|
||||||
review.
|
|
||||||
|
|
||||||
Or in other words: please apply common sense and don't blindly accept LLM
|
The goal is to avoid ambiguity in license compliance and optimize the
|
||||||
suggestions.
|
use of limited project resources, especially for code review and
|
||||||
|
maintenance. This policy can be revisited as LLMs evolve and mature.
|
||||||
This policy can be revisited as LLMs evolve and mature.
|
|
||||||
|
|
||||||
### Code Review
|
|
||||||
|
|
||||||
We generally recommend doing early coarse-grained reviews using state-of-the-art
|
|
||||||
LLMs. This can help identify rough edges, copy & paste errors, and typos early
|
|
||||||
on. This reduces review cycles for human reviewers.
|
|
||||||
|
|
||||||
Please **do not** use GitHub Copilot directly in PRs to keep discussions clean.
|
|
||||||
Instead, ask an LLM of your choice for a review. A convenient way to do this is
|
|
||||||
|
|
||||||
- appending `.patch` to the GitHub PR URL
|
|
||||||
(e.g., `https://github.com/cloud-hypervisor/cloud-hypervisor/pull/1234.patch`)
|
|
||||||
and pasting it into the LLM of your choice, or
|
|
||||||
- using a local agent in your terminal, such as `codex` or `claude`.
|
|
||||||
|
|
||||||
### Contributions assisted by LLMs
|
|
||||||
|
|
||||||
All contributions **must** be submitted by a human contributor. Automated or
|
|
||||||
bot-driven PRs are not accepted.
|
|
||||||
|
|
||||||
You are responsible for every piece of code you submit, and you must understand
|
|
||||||
both the design and the implementation details. LLMs are useful for prototyping
|
|
||||||
and generating boilerplate code. However, large or complex logic must be
|
|
||||||
authored and fully understood by the contributor - LLM output should not be
|
|
||||||
submitted without careful review and comprehension.
|
|
||||||
|
|
||||||
Please disclose LLM use in your commit message and PR description if it
|
|
||||||
meaningfully contributed to the submitted code. Again, we recommend careful and
|
|
||||||
conservative use of LLMs, guided by common sense.
|
|
||||||
|
|
||||||
Use the following tag to disclose LLM assistance in your commit message:
|
|
||||||
|
|
||||||
```
|
|
||||||
Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]
|
|
||||||
```
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
- ``AGENT_NAME`` is the name of the AI tool or framework
|
|
||||||
- ``MODEL_VERSION`` is the specific model version used
|
|
||||||
- ``[TOOL1] [TOOL2]`` are optional specialized analysis tools used
|
|
||||||
|
|
||||||
Basic development tools (git, make, editors) should not be listed.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
Assisted-by: Claude:Opus-4.6 CodeQL
|
|
||||||
```
|
|
||||||
|
|
||||||
Maintainers reserve the right to request additional clarification or decline
|
|
||||||
contributions where LLM usage raises concerns. Ultimately, acceptance of any
|
|
||||||
contribution is at the maintainers' discretion.
|
|
||||||
|
|||||||
1137
Cargo.lock
generated
1137
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
67
Cargo.toml
67
Cargo.toml
@@ -22,6 +22,7 @@ members = [
|
|||||||
"devices",
|
"devices",
|
||||||
"event_monitor",
|
"event_monitor",
|
||||||
"hypervisor",
|
"hypervisor",
|
||||||
|
"net_gen",
|
||||||
"net_util",
|
"net_util",
|
||||||
"option_parser",
|
"option_parser",
|
||||||
"pci",
|
"pci",
|
||||||
@@ -40,67 +41,57 @@ members = [
|
|||||||
"vmm",
|
"vmm",
|
||||||
]
|
]
|
||||||
package.edition = "2024"
|
package.edition = "2024"
|
||||||
# Minimum buildable version:
|
|
||||||
# Keep in sync with version in .github/workflows/build.yaml
|
|
||||||
# Policy on MSRV (see #4318):
|
|
||||||
# Can only be bumped if satisfying any of the following:
|
|
||||||
# a.) A dependency requires it,
|
|
||||||
# b.) If we want to use a new feature and that MSRV is at least 6 months old,
|
|
||||||
# c.) There is a security issue that is addressed by the toolchain update.
|
|
||||||
package.rust-version = "1.89.0"
|
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# rust-vmm crates
|
# rust-vmm crates
|
||||||
acpi_tables = "0.2.0"
|
acpi_tables = { git = "https://github.com/rust-vmm/acpi_tables", branch = "main" }
|
||||||
iommufd-ioctls = "0.1.0"
|
kvm-bindings = "0.12.1"
|
||||||
kvm-bindings = "0.14.0"
|
kvm-ioctls = "0.22.1"
|
||||||
kvm-ioctls = "0.24.0"
|
linux-loader = "0.13.1"
|
||||||
linux-loader = "0.13.2"
|
mshv-bindings = "0.6.5"
|
||||||
mshv-bindings = "0.6.9"
|
mshv-ioctls = "0.6.5"
|
||||||
mshv-ioctls = "0.6.9"
|
|
||||||
seccompiler = "0.5.0"
|
seccompiler = "0.5.0"
|
||||||
vfio-bindings = { version = "0.6.2", default-features = false }
|
vfio-bindings = { version = "0.6.0", default-features = false }
|
||||||
vfio-ioctls = { version = "0.6.0", default-features = false }
|
vfio-ioctls = { version = "0.5.1", default-features = false }
|
||||||
vfio_user = { version = "0.1.3", default-features = false }
|
vfio_user = { version = "0.1.1", default-features = false }
|
||||||
vhost = { version = "0.16.0", default-features = false }
|
vhost = { version = "0.14.0", default-features = false }
|
||||||
vhost-user-backend = { version = "0.22.0", default-features = false }
|
vhost-user-backend = { version = "0.20.0", default-features = false }
|
||||||
virtio-bindings = "0.2.6"
|
virtio-bindings = "0.2.6"
|
||||||
virtio-queue = "0.17.0"
|
virtio-queue = "0.16.0"
|
||||||
vm-fdt = "0.3.0"
|
vm-fdt = "0.3.0"
|
||||||
vm-memory = "0.17.1"
|
vm-memory = "0.16.1"
|
||||||
vmm-sys-util = "0.15.0"
|
vmm-sys-util = "0.14.0"
|
||||||
|
|
||||||
# igvm crates
|
# igvm crates
|
||||||
igvm = "0.4.0"
|
# TODO: bump to 0.3.5 release
|
||||||
igvm_defs = "0.4.0"
|
igvm = { git = "https://github.com/microsoft/igvm", branch = "main" }
|
||||||
|
igvm_defs = { git = "https://github.com/microsoft/igvm", branch = "main" }
|
||||||
|
|
||||||
# serde crates
|
# serde crates
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.145"
|
||||||
serde_with = { version = "3.18.0", default-features = false }
|
serde_with = { version = "3.16.1", default-features = false }
|
||||||
|
|
||||||
# other crates
|
# other crates
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.99"
|
||||||
bitflags = "2.11.1"
|
bitflags = "2.10.0"
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
cfg-if = "1.0.4"
|
cfg-if = "1.0.4"
|
||||||
clap = "4.6.1"
|
clap = "4.5.53"
|
||||||
dhat = "0.3.3"
|
dhat = "0.3.3"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
env_logger = "0.11.10"
|
env_logger = "0.11.8"
|
||||||
epoll = "4.4.0"
|
epoll = "4.4.0"
|
||||||
flume = "0.12.0"
|
flume = "0.12.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jiff = { version = "0.2", default-features = false, features = ["std"] }
|
libc = "0.2.178"
|
||||||
libc = "0.2.186"
|
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
sha2 = "0.11.0"
|
signal-hook = "0.3.18"
|
||||||
signal-hook = "0.4.4"
|
thiserror = "2.0.17"
|
||||||
thiserror = "2.0.18"
|
uuid = { version = "1.19.0" }
|
||||||
uuid = { version = "1.23.1" }
|
|
||||||
wait-timeout = "0.2.1"
|
wait-timeout = "0.2.1"
|
||||||
zerocopy = { version = "0.8.48", default-features = false }
|
zerocopy = { version = "0.8.31", default-features = false }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
# Any clippy lint (group) in alphabetical order:
|
# Any clippy lint (group) in alphabetical order:
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -111,25 +111,19 @@ do not wish to use the pre-built binaries.
|
|||||||
|
|
||||||
## Booting Linux
|
## Booting Linux
|
||||||
|
|
||||||
Cloud Hypervisor boots guests in one of two ways. The first is direct
|
Cloud Hypervisor supports direct kernel boot (the x86-64 kernel requires the kernel
|
||||||
kernel boot, where a kernel image is passed to `--kernel`. The x86-64
|
built with PVH support or a bzImage) or booting via a firmware (either [Rust Hypervisor
|
||||||
kernel must be built with PVH support or be a bzImage. The second is
|
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) or an
|
||||||
firmware boot, where a firmware image is passed to `--firmware` and
|
edk2 UEFI firmware called `CLOUDHV` / `CLOUDHV_EFI`.)
|
||||||
brings up the guest's normal boot loader.
|
|
||||||
|
|
||||||
Two firmware options are supported, and which one works best depends
|
Binary builds of the firmware files are available for the latest release of
|
||||||
on the guest OS. [Rust Hypervisor
|
[Rust Hypervisor
|
||||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware)
|
|
||||||
is a lightweight Rust-based PVH firmware. The edk2 UEFI firmware is
|
|
||||||
called `CLOUDHV.fd` for x86-64 and `CLOUDHV_EFI.fd` for AArch64.
|
|
||||||
Prebuilt binaries for both are available at their respective releases
|
|
||||||
pages, [Rust Hypervisor
|
|
||||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest)
|
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest)
|
||||||
and [our edk2
|
and [our edk2
|
||||||
fork](https://github.com/cloud-hypervisor/edk2/releases/latest).
|
repository](https://github.com/cloud-hypervisor/edk2/releases/latest)
|
||||||
The edk2 fork carries customizations required to boot AArch64 guests
|
|
||||||
on cloud-hypervisor. See [docs/uefi.md](docs/uefi.md) for differences
|
The choice of firmware depends on your guest OS choice; some experimentation
|
||||||
with upstream tianocore/edk2.
|
may be required.
|
||||||
|
|
||||||
### Firmware Booting
|
### Firmware Booting
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["The Cloud Hypervisor Authors"]
|
authors = ["The Cloud Hypervisor Authors"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license = "Apache-2.0"
|
|
||||||
name = "api_client"
|
name = "api_client"
|
||||||
rust-version.workspace = true
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
authors = ["The Chromium OS Authors"]
|
authors = ["The Chromium OS Authors"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
name = "arch"
|
name = "arch"
|
||||||
rust-version.workspace = true
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use hypervisor::arch::aarch64::regs::{
|
|||||||
AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ,
|
AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ,
|
||||||
AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ,
|
AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ,
|
||||||
};
|
};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, warn};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use vm_fdt::{FdtWriter, FdtWriterResult};
|
use vm_fdt::{FdtWriter, FdtWriterResult};
|
||||||
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
|
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
|
||||||
@@ -345,17 +345,6 @@ fn create_cpu_nodes(
|
|||||||
warn!("cache sysfs system does not exist.");
|
warn!("cache sysfs system does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arm boot protocol requires a minimal Device Tree
|
|
||||||
// https://docs.kernel.org/arch/arm64/booting.html
|
|
||||||
// As Generic initiators are supported only in ACPI
|
|
||||||
// When a guest kernel does not boot under "acpi=force" mode it can
|
|
||||||
// hang due to conflicting numa information present in FDT which
|
|
||||||
// does not support Generic Initiators
|
|
||||||
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
|
|
||||||
if has_generic_initiator {
|
|
||||||
info!("Skipping NUMA CPU node encoding in FDT with Generic Initiator devices");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) {
|
for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) {
|
||||||
let cpu_name = format!("cpu@{cpu_id:x}");
|
let cpu_name = format!("cpu@{cpu_id:x}");
|
||||||
let cpu_node = fdt.begin_node(&cpu_name)?;
|
let cpu_node = fdt.begin_node(&cpu_name)?;
|
||||||
@@ -370,10 +359,8 @@ fn create_cpu_nodes(
|
|||||||
fdt.property_u32("reg", (mpidr & 0x7FFFFF) as u32)?;
|
fdt.property_u32("reg", (mpidr & 0x7FFFFF) as u32)?;
|
||||||
fdt.property_u32("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?;
|
fdt.property_u32("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?;
|
||||||
|
|
||||||
// Skipping NUMA encoding in FDT when Generic Initiator devices
|
// Add `numa-node-id` property if there is any numa config.
|
||||||
// are present allowed such guest kernels to boot properly and
|
if numa_nodes.len() > 1 {
|
||||||
// rely solely on ACPI tables to setup NUMA
|
|
||||||
if numa_nodes.len() > 1 && !has_generic_initiator {
|
|
||||||
for numa_node_idx in 0..numa_nodes.len() {
|
for numa_node_idx in 0..numa_nodes.len() {
|
||||||
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||||
if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) {
|
if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) {
|
||||||
@@ -514,14 +501,7 @@ fn create_memory_node(
|
|||||||
) -> FdtWriterResult<()> {
|
) -> FdtWriterResult<()> {
|
||||||
// See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt
|
// See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt
|
||||||
// for NUMA setting in memory node.
|
// for NUMA setting in memory node.
|
||||||
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
|
if numa_nodes.len() > 1 {
|
||||||
if has_generic_initiator {
|
|
||||||
info!("Skipping NUMA memory node encoding in FDT with Generic Initiator devices");
|
|
||||||
}
|
|
||||||
// Skipping NUMA encoding in FDT when Generic Initiator devices
|
|
||||||
// are present allowed guest kernels to boot and
|
|
||||||
// rely solely on ACPI tables to setup NUMA
|
|
||||||
if numa_nodes.len() > 1 && !has_generic_initiator {
|
|
||||||
for numa_node_idx in 0..numa_nodes.len() {
|
for numa_node_idx in 0..numa_nodes.len() {
|
||||||
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||||
let mut mem_reg_prop: Vec<u64> = Vec::new();
|
let mut mem_reg_prop: Vec<u64> = Vec::new();
|
||||||
@@ -538,15 +518,12 @@ fn create_memory_node(
|
|||||||
node_memory_addr = memory_region_start_addr;
|
node_memory_addr = memory_region_start_addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only create a memory node if this NUMA node has memory regions
|
let memory_node_name = format!("memory@{node_memory_addr:x}");
|
||||||
if !mem_reg_prop.is_empty() {
|
let memory_node = fdt.begin_node(&memory_node_name)?;
|
||||||
let memory_node_name = format!("memory@{node_memory_addr:x}");
|
fdt.property_string("device_type", "memory")?;
|
||||||
let memory_node = fdt.begin_node(&memory_node_name)?;
|
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||||
fdt.property_string("device_type", "memory")?;
|
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
|
||||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
fdt.end_node(memory_node)?;
|
||||||
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
|
|
||||||
fdt.end_node(memory_node)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Note: memory regions from "GuestMemory" are sorted and non-zero sized.
|
// Note: memory regions from "GuestMemory" are sorted and non-zero sized.
|
||||||
@@ -1067,22 +1044,6 @@ fn create_pci_nodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_distance_map_node(fdt: &mut FdtWriter, numa_nodes: &NumaNodes) -> FdtWriterResult<()> {
|
fn create_distance_map_node(fdt: &mut FdtWriter, numa_nodes: &NumaNodes) -> FdtWriterResult<()> {
|
||||||
// When Generic Initiator nodes are present, skip ALL FDT NUMA information.
|
|
||||||
// Let ACPI (which supports Generic Initiator via SRAT Type 5) handle the entire NUMA topology.
|
|
||||||
// FDT cannot represent Generic Initiator nodes, and mixing FDT + ACPI NUMA info causes conflicts.
|
|
||||||
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
|
|
||||||
if has_generic_initiator {
|
|
||||||
info!("Skipping NUMA distance map encoding in FDT with Generic Initiator devices");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// At this point, we know there are no Generic Initiator nodes
|
|
||||||
let mut numa_ids: Vec<u32> = numa_nodes.keys().cloned().collect();
|
|
||||||
|
|
||||||
// If we only have one node, no distance map is needed
|
|
||||||
if numa_ids.len() <= 1 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let distance_map_node = fdt.begin_node("distance-map")?;
|
let distance_map_node = fdt.begin_node("distance-map")?;
|
||||||
fdt.property_string("compatible", "numa-distance-map-v1")?;
|
fdt.property_string("compatible", "numa-distance-map-v1")?;
|
||||||
// Construct the distance matrix.
|
// Construct the distance matrix.
|
||||||
@@ -1095,33 +1056,26 @@ fn create_distance_map_node(fdt: &mut FdtWriter, numa_nodes: &NumaNodes) -> FdtW
|
|||||||
// a value greater than 10.
|
// a value greater than 10.
|
||||||
// 4. distance-matrix should have entries in lexicographical ascending
|
// 4. distance-matrix should have entries in lexicographical ascending
|
||||||
// order of nodes.
|
// order of nodes.
|
||||||
numa_ids.sort_unstable(); // lexicographical order
|
|
||||||
let mut distance_matrix = Vec::new();
|
let mut distance_matrix = Vec::new();
|
||||||
// Iterate over actual numa IDs instead of 0..len()
|
for numa_node_idx in 0..numa_nodes.len() {
|
||||||
for numa_id in numa_ids.iter() {
|
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||||
let numa_node = &numa_nodes[numa_id];
|
for dest_numa_node in 0..numa_node.unwrap().distances.len() + 1 {
|
||||||
for dest_numa_id in numa_ids.iter() {
|
if numa_node_idx == dest_numa_node {
|
||||||
if *numa_id == *dest_numa_id {
|
distance_matrix.push(numa_node_idx as u32);
|
||||||
distance_matrix.push(*numa_id);
|
distance_matrix.push(dest_numa_node as u32);
|
||||||
distance_matrix.push(*dest_numa_id);
|
|
||||||
distance_matrix.push(10_u32);
|
distance_matrix.push(10_u32);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
distance_matrix.push(*numa_id);
|
distance_matrix.push(numa_node_idx as u32);
|
||||||
distance_matrix.push(*dest_numa_id);
|
distance_matrix.push(dest_numa_node as u32);
|
||||||
// Use user-specified distance, checking both directions for symmetry
|
distance_matrix.push(
|
||||||
let distance = if let Some(&dist) = numa_node.distances.get(dest_numa_id) {
|
*numa_node
|
||||||
// Forward direction: current node -> dest node
|
.unwrap()
|
||||||
dist
|
.distances
|
||||||
} else if let Some(dest_node) = numa_nodes.get(dest_numa_id) {
|
.get(&(dest_numa_node as u32))
|
||||||
// Reverse direction for symmetry: dest node -> current node
|
.unwrap() as u32,
|
||||||
dest_node.distances.get(numa_id).copied().unwrap_or(20)
|
);
|
||||||
} else {
|
|
||||||
// Default distance when neither direction is specified
|
|
||||||
20
|
|
||||||
};
|
|
||||||
distance_matrix.push(distance as u32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fdt.property_array_u32("distance-matrix", distance_matrix.as_ref())?;
|
fdt.property_array_u32("distance-matrix", distance_matrix.as_ref())?;
|
||||||
@@ -1206,118 +1160,3 @@ fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) {
|
|||||||
print_node(child, n_spaces + 2);
|
print_node(child, n_spaces + 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::NumaNode;
|
|
||||||
|
|
||||||
// Helper function to create a simple NumaNode for testing
|
|
||||||
fn create_test_numa_node(cpus: Vec<u32>, device_id: Option<String>) -> NumaNode {
|
|
||||||
NumaNode {
|
|
||||||
memory_regions: Vec::new(),
|
|
||||||
hotplug_regions: Vec::new(),
|
|
||||||
cpus,
|
|
||||||
pci_segments: Vec::new(),
|
|
||||||
distances: BTreeMap::new(),
|
|
||||||
memory_zones: Vec::new(),
|
|
||||||
device_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fdt_generic_initiator_detection_and_skip() {
|
|
||||||
// No Generic Initiator - should not skip FDT NUMA
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
|
|
||||||
numa_nodes.insert(1, create_test_numa_node(vec![2, 3], None));
|
|
||||||
|
|
||||||
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
|
|
||||||
assert!(
|
|
||||||
!has_gi,
|
|
||||||
"Should not detect Generic Initiator when none present"
|
|
||||||
);
|
|
||||||
|
|
||||||
// One Generic Initiator - should skip FDT NUMA
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
|
|
||||||
numa_nodes.insert(1, create_test_numa_node(vec![], Some("vfio0".to_string())));
|
|
||||||
|
|
||||||
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
|
|
||||||
assert!(has_gi, "Should detect Generic Initiator when present");
|
|
||||||
|
|
||||||
let mut fdt = FdtWriter::new().unwrap();
|
|
||||||
let result = create_distance_map_node(&mut fdt, &numa_nodes);
|
|
||||||
assert!(result.is_ok(), "Should skip distance map when GI present");
|
|
||||||
|
|
||||||
// Multiple Generic Initiators - should skip FDT NUMA
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
|
|
||||||
numa_nodes.insert(1, create_test_numa_node(vec![], Some("vfio0".to_string())));
|
|
||||||
numa_nodes.insert(2, create_test_numa_node(vec![], Some("vfio1".to_string())));
|
|
||||||
|
|
||||||
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
|
|
||||||
assert!(has_gi, "Should detect multiple Generic Initiators");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fdt_distance_map() {
|
|
||||||
// Single NUMA node - should skip distance map
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
|
|
||||||
|
|
||||||
let mut fdt = FdtWriter::new().unwrap();
|
|
||||||
let result = create_distance_map_node(&mut fdt, &numa_nodes);
|
|
||||||
assert!(result.is_ok(), "Should skip distance map for single node");
|
|
||||||
|
|
||||||
// Empty NUMA nodes - should handle gracefully
|
|
||||||
let numa_nodes = BTreeMap::new();
|
|
||||||
let mut fdt = FdtWriter::new().unwrap();
|
|
||||||
let result = create_distance_map_node(&mut fdt, &numa_nodes);
|
|
||||||
assert!(result.is_ok(), "Should handle empty NUMA nodes");
|
|
||||||
|
|
||||||
// Non-contiguous NUMA IDs (0, 2, 5) with distance symmetry
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
|
|
||||||
let mut node0 = create_test_numa_node(vec![0], None);
|
|
||||||
node0.distances.insert(2, 20);
|
|
||||||
// node0 has no explicit distance to node5
|
|
||||||
|
|
||||||
let mut node2 = create_test_numa_node(vec![1], None);
|
|
||||||
node2.distances.insert(0, 20);
|
|
||||||
node2.distances.insert(5, 25);
|
|
||||||
|
|
||||||
let mut node5 = create_test_numa_node(vec![2], None);
|
|
||||||
node5.distances.insert(0, 30);
|
|
||||||
node5.distances.insert(2, 25);
|
|
||||||
// node5->node0 (should be used for node0->node5)
|
|
||||||
|
|
||||||
numa_nodes.insert(0, node0);
|
|
||||||
numa_nodes.insert(2, node2);
|
|
||||||
numa_nodes.insert(5, node5);
|
|
||||||
|
|
||||||
// Verify IDs are sorted lexicographically
|
|
||||||
let mut numa_ids: Vec<u32> = numa_nodes.keys().cloned().collect();
|
|
||||||
numa_ids.sort_unstable();
|
|
||||||
assert_eq!(numa_ids, vec![0, 2, 5]);
|
|
||||||
|
|
||||||
let mut fdt = FdtWriter::new().unwrap();
|
|
||||||
let result = create_distance_map_node(&mut fdt, &numa_nodes);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"Should handle non-contiguous IDs and symmetry"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Default distance (20) when no distance specified in either direction
|
|
||||||
let mut numa_nodes = BTreeMap::new();
|
|
||||||
numa_nodes.insert(0, create_test_numa_node(vec![0], None));
|
|
||||||
numa_nodes.insert(1, create_test_numa_node(vec![1], None));
|
|
||||||
// Neither node has distance to the other
|
|
||||||
|
|
||||||
let mut fdt = FdtWriter::new().unwrap();
|
|
||||||
let result = create_distance_map_node(&mut fdt, &numa_nodes);
|
|
||||||
assert!(result.is_ok(), "Should default to 20 for missing distances");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::os::fd::AsFd;
|
|||||||
use std::result;
|
use std::result;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use vm_memory::{Bytes, GuestAddress, GuestMemory};
|
use vm_memory::{GuestAddress, GuestMemory};
|
||||||
|
|
||||||
/// Errors thrown while loading UEFI binary
|
/// Errors thrown while loading UEFI binary
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ pub struct NumaNode {
|
|||||||
pub pci_segments: Vec<u16>,
|
pub pci_segments: Vec<u16>,
|
||||||
pub distances: BTreeMap<u32, u8>,
|
pub distances: BTreeMap<u32, u8>,
|
||||||
pub memory_zones: Vec<String>,
|
pub memory_zones: Vec<String>,
|
||||||
pub device_id: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NumaNodes = BTreeMap<u32, NumaNode>;
|
pub type NumaNodes = BTreeMap<u32, NumaNode>;
|
||||||
|
|||||||
@@ -641,16 +641,17 @@ pub fn generate_common_cpuid(
|
|||||||
|
|
||||||
// Update some existing CPUID
|
// Update some existing CPUID
|
||||||
for entry in cpuid.as_mut_slice().iter_mut() {
|
for entry in cpuid.as_mut_slice().iter_mut() {
|
||||||
#[allow(unused_unsafe)]
|
|
||||||
match entry.function {
|
match entry.function {
|
||||||
// Clear AMX related bits if the AMX feature is not enabled
|
// Clear AMX related bits if the AMX feature is not enabled
|
||||||
0x7 if !config.amx => {
|
0x7 => {
|
||||||
if entry.index == 0 {
|
if !config.amx {
|
||||||
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
|
if entry.index == 0 {
|
||||||
}
|
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
|
||||||
if entry.index == 1 {
|
}
|
||||||
entry.eax &= !(1 << AMX_FP16);
|
if entry.index == 1 {
|
||||||
entry.edx &= !(1 << AMX_COMPLEX);
|
entry.eax &= !(1 << AMX_FP16);
|
||||||
|
entry.edx &= !(1 << AMX_COMPLEX);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0xd =>
|
0xd =>
|
||||||
@@ -672,52 +673,55 @@ pub fn generate_common_cpuid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Tile Information (purely AMX related).
|
0x1d => {
|
||||||
0x1d if !config.amx => {
|
// Tile Information (purely AMX related).
|
||||||
entry.eax = 0;
|
if !config.amx {
|
||||||
entry.ebx = 0;
|
entry.eax = 0;
|
||||||
entry.ecx = 0;
|
entry.ebx = 0;
|
||||||
entry.edx = 0;
|
entry.ecx = 0;
|
||||||
|
entry.edx = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TMUL information (purely AMX related)
|
0x1e => {
|
||||||
0x1e if !config.amx => {
|
// TMUL information (purely AMX related)
|
||||||
entry.eax = 0;
|
if !config.amx {
|
||||||
entry.ebx = 0;
|
entry.eax = 0;
|
||||||
entry.ecx = 0;
|
entry.ebx = 0;
|
||||||
entry.edx = 0;
|
entry.ecx = 0;
|
||||||
|
entry.edx = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy host L1 cache details if not populated by KVM
|
// Copy host L1 cache details if not populated by KVM
|
||||||
0x8000_0005
|
0x8000_0005 => {
|
||||||
if entry.eax == 0
|
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
|
||||||
&& entry.ebx == 0
|
#[allow(unused_unsafe)]
|
||||||
&& entry.ecx == 0
|
|
||||||
&& entry.edx == 0
|
|
||||||
// SAFETY: cpuid called with valid leaves
|
// SAFETY: cpuid called with valid leaves
|
||||||
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
|
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 {
|
||||||
{
|
// SAFETY: cpuid called with valid leaves
|
||||||
// SAFETY: cpuid called with valid leaves
|
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
|
||||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
|
entry.eax = leaf.eax;
|
||||||
entry.eax = leaf.eax;
|
entry.ebx = leaf.ebx;
|
||||||
entry.ebx = leaf.ebx;
|
entry.ecx = leaf.ecx;
|
||||||
entry.ecx = leaf.ecx;
|
entry.edx = leaf.edx;
|
||||||
entry.edx = leaf.edx;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Copy host L2 cache details if not populated by KVM
|
// Copy host L2 cache details if not populated by KVM
|
||||||
0x8000_0006
|
0x8000_0006 => {
|
||||||
if entry.eax == 0
|
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
|
||||||
&& entry.ebx == 0
|
#[allow(unused_unsafe)]
|
||||||
&& entry.ecx == 0
|
|
||||||
&& entry.edx == 0
|
|
||||||
// SAFETY: cpuid called with valid leaves
|
// SAFETY: cpuid called with valid leaves
|
||||||
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
|
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 {
|
||||||
{
|
#[allow(unused_unsafe)]
|
||||||
// SAFETY: cpuid called with valid leaves
|
// SAFETY: cpuid called with valid leaves
|
||||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
|
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
|
||||||
entry.eax = leaf.eax;
|
entry.eax = leaf.eax;
|
||||||
entry.ebx = leaf.ebx;
|
entry.ebx = leaf.ebx;
|
||||||
entry.ecx = leaf.ecx;
|
entry.ecx = leaf.ecx;
|
||||||
entry.edx = leaf.edx;
|
entry.edx = leaf.edx;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Set CPU physical bits
|
// Set CPU physical bits
|
||||||
0x8000_0008 => {
|
0x8000_0008 => {
|
||||||
@@ -819,7 +823,6 @@ pub fn configure_vcpu(
|
|||||||
cpu_vendor: CpuVendor,
|
cpu_vendor: CpuVendor,
|
||||||
topology: (u16, u16, u16, u16),
|
topology: (u16, u16, u16, u16),
|
||||||
nested: bool,
|
nested: bool,
|
||||||
setup_registers: bool,
|
|
||||||
) -> super::Result<()> {
|
) -> super::Result<()> {
|
||||||
let x2apic_id = get_x2apic_id(id, Some(topology));
|
let x2apic_id = get_x2apic_id(id, Some(topology));
|
||||||
|
|
||||||
@@ -838,13 +841,11 @@ pub fn configure_vcpu(
|
|||||||
entry.ebx &= 0xffffff;
|
entry.ebx &= 0xffffff;
|
||||||
entry.ebx |= x2apic_id << 24;
|
entry.ebx |= x2apic_id << 24;
|
||||||
apic_id_patched = true;
|
apic_id_patched = true;
|
||||||
if matches!(cpu_vendor, CpuVendor::Intel) {
|
if !nested {
|
||||||
if !nested {
|
// Disable nested virtualization for Intel
|
||||||
// Disable nested virtualization for Intel
|
entry.ecx &= !(1 << VMX_ECX_BIT);
|
||||||
entry.ecx &= !(1 << VMX_ECX_BIT);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if entry.function == 0x8000_0001 {
|
if entry.function == 0x8000_0001 {
|
||||||
if !nested {
|
if !nested {
|
||||||
@@ -893,19 +894,17 @@ pub fn configure_vcpu(
|
|||||||
|
|
||||||
regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?;
|
regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?;
|
||||||
if let Some((kernel_entry_point, guest_memory)) = boot_setup {
|
if let Some((kernel_entry_point, guest_memory)) = boot_setup {
|
||||||
if setup_registers {
|
regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?;
|
||||||
regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?;
|
|
||||||
|
|
||||||
// CPUs are required (by Intel sdm spec) to boot in x2apic mode if any
|
|
||||||
// of the apic IDs is larger than 255. Experimentally, the Linux kernel
|
|
||||||
// does not recognize the last vCPU if x2apic is not enabled when
|
|
||||||
// there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255),
|
|
||||||
// so we need to enable x2apic in this case as well.
|
|
||||||
let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY;
|
|
||||||
regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode)
|
|
||||||
.map_err(Error::SregsConfiguration)?;
|
|
||||||
}
|
|
||||||
regs::setup_fpu(vcpu).map_err(Error::FpuConfiguration)?;
|
regs::setup_fpu(vcpu).map_err(Error::FpuConfiguration)?;
|
||||||
|
|
||||||
|
// CPUs are required (by Intel sdm spec) to boot in x2apic mode if any
|
||||||
|
// of the apic IDs is larger than 255. Experimentally, the Linux kernel
|
||||||
|
// does not recognize the last vCPU if x2apic is not enabled when
|
||||||
|
// there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255),
|
||||||
|
// so we need to enable x2apic in this case as well.
|
||||||
|
let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY;
|
||||||
|
regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode)
|
||||||
|
.map_err(Error::SregsConfiguration)?;
|
||||||
}
|
}
|
||||||
interrupts::set_lint(vcpu).map_err(|e| Error::LocalIntConfiguration(e.into()))?;
|
interrupts::set_lint(vcpu).map_err(|e| Error::LocalIntConfiguration(e.into()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -101,9 +101,8 @@ const CPU_FEATURE_APIC: u32 = 0x200;
|
|||||||
const CPU_FEATURE_FPU: u32 = 0x001;
|
const CPU_FEATURE_FPU: u32 = 0x001;
|
||||||
|
|
||||||
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
|
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
|
||||||
let v: *const T = v;
|
|
||||||
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
||||||
let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::<T>()) };
|
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
|
||||||
let mut checksum: u8 = 0;
|
let mut checksum: u8 = 0;
|
||||||
for i in v_slice.iter() {
|
for i in v_slice.iter() {
|
||||||
checksum = checksum.wrapping_add(*i);
|
checksum = checksum.wrapping_add(*i);
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ pub enum Error {
|
|||||||
#[error("Failure to write additional data to memory")]
|
#[error("Failure to write additional data to memory")]
|
||||||
WriteData,
|
WriteData,
|
||||||
/// Failure to parse uuid, uuid format may be error
|
/// Failure to parse uuid, uuid format may be error
|
||||||
#[error("Failure to parse uuid: {1}")]
|
#[error("Failure to parse uuid")]
|
||||||
ParseUuid(#[source] uuid::Error, String),
|
ParseUuid(#[source] uuid::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Error>;
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
@@ -49,9 +49,8 @@ const PCI_SUPPORTED: u64 = 1 << 7;
|
|||||||
const IS_VIRTUAL_MACHINE: u8 = 1 << 4;
|
const IS_VIRTUAL_MACHINE: u8 = 1 << 4;
|
||||||
|
|
||||||
fn compute_checksum<T: Copy>(v: &T) -> u8 {
|
fn compute_checksum<T: Copy>(v: &T) -> u8 {
|
||||||
let v: *const T = v;
|
|
||||||
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
||||||
let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::<T>()) };
|
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
|
||||||
let mut checksum: u8 = 0;
|
let mut checksum: u8 = 0;
|
||||||
for i in v_slice.iter() {
|
for i in v_slice.iter() {
|
||||||
checksum = checksum.wrapping_add(*i);
|
checksum = checksum.wrapping_add(*i);
|
||||||
@@ -199,7 +198,7 @@ pub fn setup_smbios(
|
|||||||
let uuid_number = uuid
|
let uuid_number = uuid
|
||||||
.map(Uuid::parse_str)
|
.map(Uuid::parse_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))?
|
.map_err(Error::ParseUuid)?
|
||||||
.unwrap_or(Uuid::nil());
|
.unwrap_or(Uuid::nil());
|
||||||
let smbios_sysinfo = SmbiosSysInfo {
|
let smbios_sysinfo = SmbiosSysInfo {
|
||||||
r#type: SYSTEM_INFORMATION,
|
r#type: SYSTEM_INFORMATION,
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
|
|||||||
// SAFETY: we read exactly the size of the descriptor header
|
// SAFETY: we read exactly the size of the descriptor header
|
||||||
file.read_exact(unsafe {
|
file.read_exact(unsafe {
|
||||||
std::slice::from_raw_parts_mut(
|
std::slice::from_raw_parts_mut(
|
||||||
(&raw mut descriptor).cast(),
|
&mut descriptor as *mut _ as *mut u8,
|
||||||
std::mem::size_of::<TdvfDescriptor>(),
|
std::mem::size_of::<TdvfDescriptor>(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -190,7 +190,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
|
|||||||
// SAFETY: we read exactly the advertised sections
|
// SAFETY: we read exactly the advertised sections
|
||||||
file.read_exact(unsafe {
|
file.read_exact(unsafe {
|
||||||
std::slice::from_raw_parts_mut(
|
std::slice::from_raw_parts_mut(
|
||||||
sections.as_mut_ptr().cast(),
|
sections.as_mut_ptr() as *mut u8,
|
||||||
descriptor.num_sections as usize * std::mem::size_of::<TdvfSection>(),
|
descriptor.num_sections as usize * std::mem::size_of::<TdvfSection>(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"]
|
authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
name = "block"
|
name = "block"
|
||||||
rust-version.workspace = true
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -10,11 +9,10 @@ default = []
|
|||||||
io_uring = ["dep:io-uring"]
|
io_uring = ["dep:io-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = { workspace = true }
|
|
||||||
byteorder = { workspace = true }
|
byteorder = { workspace = true }
|
||||||
crc-any = "2.5.0"
|
crc-any = "2.5.0"
|
||||||
flate2 = "1.1"
|
flate2 = "1.0"
|
||||||
io-uring = { version = "0.7.12", optional = true }
|
io-uring = { version = "0.7.11", optional = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
remain = "0.2.15"
|
remain = "0.2.15"
|
||||||
@@ -33,8 +31,5 @@ vm-virtio = { path = "../vm-virtio" }
|
|||||||
vmm-sys-util = { workspace = true }
|
vmm-sys-util = { workspace = true }
|
||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
cfg-if = { workspace = true }
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use vm_memory::GuestAddress;
|
|
||||||
|
|
||||||
/// Owns an aligned bounce buffer used when a guest descriptor's host VA
|
|
||||||
/// does not meet the disk backend's alignment requirement.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AlignedOperation {
|
|
||||||
data_addr: GuestAddress,
|
|
||||||
aligned_ptr: *mut u8,
|
|
||||||
size: usize,
|
|
||||||
layout: Layout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlignedOperation {
|
|
||||||
/// Allocate a zero-initialized buffer of `size` bytes aligned to
|
|
||||||
/// `alignment`. Returns `InvalidInput` if `size` is zero;
|
|
||||||
/// `alignment` must be a power of two and not exceed `isize::MAX`
|
|
||||||
/// after rounding up.
|
|
||||||
pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result<Self> {
|
|
||||||
if size == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"AlignedOperation requires a non-zero size",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let layout = Layout::from_size_align(size, alignment)
|
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
|
||||||
// SAFETY: size is non-zero (checked above) and Layout::from_size_align
|
|
||||||
// rejects alignments that are not a power of two or that overflow.
|
|
||||||
let aligned_ptr = unsafe { alloc_zeroed(layout) };
|
|
||||||
if aligned_ptr.is_null() {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
data_addr,
|
|
||||||
aligned_ptr,
|
|
||||||
size,
|
|
||||||
layout,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the raw pointer to the aligned buffer.
|
|
||||||
pub fn as_mut_ptr(&mut self) -> *mut u8 {
|
|
||||||
self.aligned_ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the aligned buffer as a slice.
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
// SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they
|
|
||||||
// are initialized) and AlignedOperation owns the buffer
|
|
||||||
// exclusively.
|
|
||||||
unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the aligned buffer as a mutable slice.
|
|
||||||
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
|
|
||||||
// SAFETY: same invariant as as_bytes; &mut self rules out other
|
|
||||||
// simultaneous borrows.
|
|
||||||
unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the guest address for this op.
|
|
||||||
pub fn data_addr(&self) -> GuestAddress {
|
|
||||||
self.data_addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for AlignedOperation {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// SAFETY: `new` is the only constructor, and it stores a pointer
|
|
||||||
// returned by `alloc_zeroed` paired with the exact `layout` used
|
|
||||||
// for that allocation. Ownership has not escaped (the type is
|
|
||||||
// neither `Clone` nor `Copy`).
|
|
||||||
unsafe {
|
|
||||||
dealloc(self.aligned_ptr, self.layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/
|
|
||||||
// Copy, no shared aliases) and the allocation's lifetime is tied to the
|
|
||||||
// value's. Moving an AlignedOperation between threads transfers that
|
|
||||||
// ownership — the same rationale Box<T> uses for its Send impl.
|
|
||||||
unsafe impl Send for AlignedOperation {}
|
|
||||||
@@ -8,7 +8,7 @@ use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::{BatchRequest, SECTOR_SIZE};
|
use crate::{BatchRequest, DiskTopology};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DiskFileError {
|
pub enum DiskFileError {
|
||||||
@@ -24,16 +24,14 @@ pub enum DiskFileError {
|
|||||||
/// Resize failed
|
/// Resize failed
|
||||||
#[error("Resize failed")]
|
#[error("Resize failed")]
|
||||||
ResizeError(#[source] std::io::Error),
|
ResizeError(#[source] std::io::Error),
|
||||||
#[error("Failed cloning disk file")]
|
|
||||||
Clone(#[source] std::io::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DiskFileResult<T> = std::result::Result<T, DiskFileError>;
|
pub type DiskFileResult<T> = std::result::Result<T, DiskFileError>;
|
||||||
|
|
||||||
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding disk file.
|
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding [`DiskFile`].
|
||||||
///
|
///
|
||||||
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
|
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
|
||||||
/// by some disk implementations, which wrap the effective [`File`]
|
/// by some implementations of [`DiskFile`], which wrap the effective [`File`]
|
||||||
/// in an `Arc<Mutex<T>>`, making the use of [`BorrowedFd`] impossible.
|
/// in an `Arc<Mutex<T>>`, making the use of [`BorrowedFd`] impossible.
|
||||||
///
|
///
|
||||||
/// [`BorrowedFd`]: std::os::fd::BorrowedFd
|
/// [`BorrowedFd`]: std::os::fd::BorrowedFd
|
||||||
@@ -58,6 +56,35 @@ impl AsRawFd for BorrowedDiskFd<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abstraction over the effective [`File`] backing up a block device,
|
||||||
|
/// with support for synchronous and asynchronous I/O.
|
||||||
|
///
|
||||||
|
/// This allows abstracting over raw image formats as well as structured
|
||||||
|
/// image formats.
|
||||||
|
pub trait DiskFile: Send {
|
||||||
|
/// Returns the logical disk size a guest will see.
|
||||||
|
///
|
||||||
|
/// For raw formats, this is equal to [`Self::physical_size`]. For file formats
|
||||||
|
/// that wrap disk images in a container (e.g. QCOW2), this refers to the
|
||||||
|
/// effective size that the guest will see.
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64>;
|
||||||
|
/// Returns the physical size of the underlying file.
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64>;
|
||||||
|
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>>;
|
||||||
|
fn topology(&mut self) -> DiskTopology {
|
||||||
|
DiskTopology::default()
|
||||||
|
}
|
||||||
|
fn resize(&mut self, _size: u64) -> DiskFileResult<()> {
|
||||||
|
Err(DiskFileError::Unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the file descriptor of the underlying disk image file.
|
||||||
|
///
|
||||||
|
/// The file descriptor is supposed to be used for `fcntl()` calls but no
|
||||||
|
/// other operation.
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AsyncIoError {
|
pub enum AsyncIoError {
|
||||||
/// Failed vectored reading from file.
|
/// Failed vectored reading from file.
|
||||||
@@ -69,12 +96,6 @@ pub enum AsyncIoError {
|
|||||||
/// Failed synchronizing file.
|
/// Failed synchronizing file.
|
||||||
#[error("Failed synchronizing file")]
|
#[error("Failed synchronizing file")]
|
||||||
Fsync(#[source] std::io::Error),
|
Fsync(#[source] std::io::Error),
|
||||||
/// Failed punching hole.
|
|
||||||
#[error("Failed punching hole")]
|
|
||||||
PunchHole(#[source] std::io::Error),
|
|
||||||
/// Failed writing zeroes.
|
|
||||||
#[error("Failed writing zeroes")]
|
|
||||||
WriteZeroes(#[source] std::io::Error),
|
|
||||||
/// Failed submitting batch requests.
|
/// Failed submitting batch requests.
|
||||||
#[error("Failed submitting batch requests")]
|
#[error("Failed submitting batch requests")]
|
||||||
SubmitBatchRequests(#[source] std::io::Error),
|
SubmitBatchRequests(#[source] std::io::Error),
|
||||||
@@ -97,8 +118,6 @@ pub trait AsyncIo: Send {
|
|||||||
user_data: u64,
|
user_data: u64,
|
||||||
) -> AsyncIoResult<()>;
|
) -> AsyncIoResult<()>;
|
||||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
|
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
|
||||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
|
|
||||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
|
|
||||||
fn next_completed_request(&mut self) -> Option<(u64, i32)>;
|
fn next_completed_request(&mut self) -> Option<(u64, i32)>;
|
||||||
fn batch_requests_enabled(&self) -> bool {
|
fn batch_requests_enabled(&self) -> bool {
|
||||||
false
|
false
|
||||||
@@ -106,7 +125,4 @@ pub trait AsyncIo: Send {
|
|||||||
fn submit_batch_requests(&mut self, _batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
|
fn submit_batch_requests(&mut self, _batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn alignment(&self) -> u64 {
|
|
||||||
SECTOR_SIZE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
//! Composable disk capability traits for the block crate.
|
|
||||||
//!
|
|
||||||
//! Small traits define individual capabilities:
|
|
||||||
//!
|
|
||||||
//! - [`DiskSize`] - reported capacity (logical size)
|
|
||||||
//! - [`PhysicalSize`] - host allocation size
|
|
||||||
//! - [`DiskFd`] - backing file descriptor access
|
|
||||||
//! - [`Geometry`] - sector/cluster geometry (default 512B)
|
|
||||||
//! - [`SparseCapable`] - sparse and zero flag support
|
|
||||||
//! - [`Resizable`] - online resize
|
|
||||||
//!
|
|
||||||
//! [`DiskFile`] is a supertrait that bundles the universal capabilities
|
|
||||||
//! (`DiskSize` + `Geometry`). [`FullDiskFile`] adds all optional
|
|
||||||
//! capabilities. [`AsyncDiskFile`] extends `DiskFile` with async I/O
|
|
||||||
//! construction for virtio queue workers. [`AsyncFullDiskFile`]
|
|
||||||
//! combines both axes.
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! DiskFile: DiskSize + Geometry + Sync
|
|
||||||
//! / \
|
|
||||||
//! FullDiskFile: AsyncDiskFile:
|
|
||||||
//! DiskFile + PhysicalSize + DiskFile + Unpin
|
|
||||||
//! DiskFd + SparseCapable + try_clone, create_async_io
|
|
||||||
//! Resizable
|
|
||||||
//! \ /
|
|
||||||
//! AsyncFullDiskFile: FullDiskFile + AsyncDiskFile
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Readonly accessors take `&self`. Only [`Resizable::resize`] requires
|
|
||||||
//! `&mut self`. Errors are returned as [`BlockResult`].
|
|
||||||
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, BorrowedDiskFd};
|
|
||||||
use crate::{BlockResult, DiskTopology};
|
|
||||||
|
|
||||||
/// Reported capacity of a disk image.
|
|
||||||
pub trait DiskSize: Send + Debug {
|
|
||||||
/// Virtual size of the disk image in bytes (reported capacity).
|
|
||||||
fn logical_size(&self) -> BlockResult<u64>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Host allocation size of a file-backed disk image.
|
|
||||||
pub trait PhysicalSize: Send + Debug {
|
|
||||||
/// Actual bytes occupied on the host filesystem.
|
|
||||||
fn physical_size(&self) -> BlockResult<u64>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Backing file descriptor access for disk images backed by a file.
|
|
||||||
pub trait DiskFd: Send + Debug {
|
|
||||||
/// Borrows the underlying file descriptor.
|
|
||||||
fn fd(&self) -> BorrowedDiskFd<'_>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sector and cluster geometry of a disk image.
|
|
||||||
///
|
|
||||||
/// Default returns `DiskTopology::default()` (512B logical/physical).
|
|
||||||
pub trait Geometry: Send + Debug {
|
|
||||||
/// Returns the disk topology.
|
|
||||||
fn topology(&self) -> DiskTopology {
|
|
||||||
DiskTopology::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sparse and zero flag support for thin provisioned disk images.
|
|
||||||
pub trait SparseCapable: Send + Debug {
|
|
||||||
/// Indicates support for sparse operations (punch hole, write zeroes, discard).
|
|
||||||
fn supports_sparse_operations(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates support for a metadata level zero flag optimization in
|
|
||||||
/// virtio `VIRTIO_BLK_T_WRITE_ZEROES` requests. When true, the format
|
|
||||||
/// can mark regions as reading zeros via a metadata bit rather than
|
|
||||||
/// writing actual zero bytes to disk.
|
|
||||||
fn supports_zero_flag(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Live disk resize support.
|
|
||||||
///
|
|
||||||
/// Implementations may return an error if the backend does not
|
|
||||||
/// support resizing (e.g. fixed size formats).
|
|
||||||
pub trait Resizable: Send + Debug {
|
|
||||||
/// Resizes the disk image to the given size in bytes, if the backend supports it.
|
|
||||||
fn resize(&mut self, size: u64) -> BlockResult<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supertrait bundling universal disk capabilities.
|
|
||||||
///
|
|
||||||
/// Every disk format implements `DiskSize` and `Geometry`.
|
|
||||||
/// `Sync` is required so that `Arc<dyn DiskFile>` can be shared
|
|
||||||
/// across threads for concurrent readonly access.
|
|
||||||
pub trait DiskFile: DiskSize + Geometry + Sync {}
|
|
||||||
|
|
||||||
/// Full capability disk file trait.
|
|
||||||
///
|
|
||||||
/// Bundles all optional capabilities on top of [`DiskFile`]:
|
|
||||||
/// file descriptor access, physical size, sparse operations, and resize.
|
|
||||||
/// Used by consumers that need feature negotiation without async I/O
|
|
||||||
/// (e.g. vhost user block).
|
|
||||||
pub trait FullDiskFile: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable {}
|
|
||||||
|
|
||||||
/// Blanket implementation: any type implementing all constituent traits
|
|
||||||
/// automatically satisfies [`FullDiskFile`].
|
|
||||||
impl<T: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable> FullDiskFile for T {}
|
|
||||||
|
|
||||||
/// Extended disk file trait for virtio queue workers.
|
|
||||||
///
|
|
||||||
/// Adds cloning and async I/O construction on top of [`DiskFile`].
|
|
||||||
/// `Unpin` is required so trait objects can be moved freely.
|
|
||||||
pub trait AsyncDiskFile: DiskFile + Unpin {
|
|
||||||
/// Creates an independent handle for a queue worker.
|
|
||||||
///
|
|
||||||
/// The clone shares internally reference counted state (e.g.
|
|
||||||
/// `Arc<Metadata>`) with the original, but owns its own file
|
|
||||||
/// descriptor and I/O completion resources. Each virtio queue
|
|
||||||
/// gets one clone so that workers can operate in parallel
|
|
||||||
/// without contending on I/O state.
|
|
||||||
///
|
|
||||||
/// Returns `Box<dyn AsyncDiskFile>` (not `AsyncFullDiskFile`)
|
|
||||||
/// because clones only serve as data plane handles for queue
|
|
||||||
/// workers. The original remains the control plane for feature
|
|
||||||
/// negotiation and configuration.
|
|
||||||
fn try_clone(&self) -> BlockResult<Box<dyn AsyncDiskFile>>;
|
|
||||||
|
|
||||||
/// Constructs a per queue async I/O engine.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `ring_depth` - maximum number of in flight I/O operations.
|
|
||||||
/// Callers typically pass the virtio queue size. Must be greater
|
|
||||||
/// than zero. Backends that do not use an async ring (e.g. sync
|
|
||||||
/// fallback implementations) may ignore this value.
|
|
||||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full capability async disk file trait.
|
|
||||||
///
|
|
||||||
/// Combines [`FullDiskFile`] (all optional capabilities) with
|
|
||||||
/// [`AsyncDiskFile`] (async I/O construction). This is the top level
|
|
||||||
/// trait for virtio block devices that need both feature negotiation
|
|
||||||
/// and async queue workers.
|
|
||||||
///
|
|
||||||
/// The type narrowing on [`AsyncDiskFile::try_clone`] is intentional:
|
|
||||||
/// clones only serve as data plane handles for queue workers, while
|
|
||||||
/// the original `AsyncFullDiskFile` handle remains the control plane
|
|
||||||
/// for feature negotiation and configuration.
|
|
||||||
pub trait AsyncFullDiskFile: FullDiskFile + AsyncDiskFile {}
|
|
||||||
|
|
||||||
/// Blanket implementation: any type implementing both [`FullDiskFile`]
|
|
||||||
/// and [`AsyncDiskFile`] automatically satisfies [`AsyncFullDiskFile`].
|
|
||||||
impl<T: FullDiskFile + AsyncDiskFile> AsyncFullDiskFile for T {}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
//! Unified error handling for the block crate.
|
|
||||||
//!
|
|
||||||
//! # Architecture
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! BlockError -- single public error type
|
|
||||||
//! |-- BlockErrorKind -- small, stable, matchable classification
|
|
||||||
//! |-- ErrorContext -- optional diagnostic metadata (path, offset, op)
|
|
||||||
//! +-- source -- format-specific error (boxed)
|
|
||||||
//! |-- QcowError
|
|
||||||
//! |-- VhdError / RawError / ...
|
|
||||||
//! +-- io::Error / etc.
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::error::Error as StdError;
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Small, stable classification of block errors.
|
|
||||||
///
|
|
||||||
/// Callers match on this for control flow. Adding new format specific
|
|
||||||
/// errors does not require new variants here.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum BlockErrorKind {
|
|
||||||
/// An underlying I/O operation failed.
|
|
||||||
Io,
|
|
||||||
/// The disk image format is structurally invalid.
|
|
||||||
InvalidFormat,
|
|
||||||
/// The disk image requires a feature that is not implemented.
|
|
||||||
UnsupportedFeature,
|
|
||||||
/// The image is marked or detected as corrupt.
|
|
||||||
CorruptImage,
|
|
||||||
/// An address, offset, or index is outside the valid range.
|
|
||||||
OutOfBounds,
|
|
||||||
/// A file or required internal structure could not be found.
|
|
||||||
NotFound,
|
|
||||||
/// An internal counter or limit was exceeded.
|
|
||||||
Overflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for BlockErrorKind {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Io => write!(f, "I/O error"),
|
|
||||||
Self::InvalidFormat => write!(f, "Invalid format"),
|
|
||||||
Self::UnsupportedFeature => write!(f, "Unsupported feature"),
|
|
||||||
Self::CorruptImage => write!(f, "Corrupt image"),
|
|
||||||
Self::OutOfBounds => write!(f, "Out of bounds"),
|
|
||||||
Self::NotFound => write!(f, "Not found"),
|
|
||||||
Self::Overflow => write!(f, "Overflow"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Classification of the operation that was in progress when an error occurred.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum ErrorOp {
|
|
||||||
/// Opening a disk image file.
|
|
||||||
Open,
|
|
||||||
/// Detecting the image format.
|
|
||||||
DetectImageType,
|
|
||||||
/// Duplicating a backing-file descriptor.
|
|
||||||
DupBackingFd,
|
|
||||||
/// Resizing a disk image.
|
|
||||||
Resize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ErrorOp {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Open => write!(f, "open"),
|
|
||||||
Self::DetectImageType => write!(f, "detect_image_type"),
|
|
||||||
Self::DupBackingFd => write!(f, "dup_backing_fd"),
|
|
||||||
Self::Resize => write!(f, "resize"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional diagnostic context attached to a [`BlockError`].
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct ErrorContext {
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
pub offset: Option<u64>,
|
|
||||||
pub op: Option<ErrorOp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ErrorContext {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
let mut first = true;
|
|
||||||
if let Some(path) = &self.path {
|
|
||||||
write!(f, "path={}", path.display())?;
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
if let Some(offset) = self.offset {
|
|
||||||
if !first {
|
|
||||||
write!(f, " ")?;
|
|
||||||
}
|
|
||||||
write!(f, "offset={offset:#x}")?;
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
if let Some(op) = self.op {
|
|
||||||
if !first {
|
|
||||||
write!(f, " ")?;
|
|
||||||
}
|
|
||||||
write!(f, "op={op}")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unified error type for the block crate.
|
|
||||||
///
|
|
||||||
/// Pairs a stable [`BlockErrorKind`] classification with an optional
|
|
||||||
/// boxed source error (format-specific) and optional [`ErrorContext`].
|
|
||||||
///
|
|
||||||
/// Display renders kind + context only; the underlying cause is
|
|
||||||
/// exposed via [`std::error::Error::source()`] for reporters that
|
|
||||||
/// walk the chain.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BlockError {
|
|
||||||
kind: BlockErrorKind,
|
|
||||||
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
|
|
||||||
ctx: Option<ErrorContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlockError {
|
|
||||||
/// Create a new `BlockError` from a kind and a source error.
|
|
||||||
pub fn new<E>(kind: BlockErrorKind, source: E) -> Self
|
|
||||||
where
|
|
||||||
E: StdError + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
kind,
|
|
||||||
source: Some(Box::new(source)),
|
|
||||||
ctx: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a `BlockError` from just a kind, with no underlying cause.
|
|
||||||
pub fn from_kind(kind: BlockErrorKind) -> Self {
|
|
||||||
Self {
|
|
||||||
kind,
|
|
||||||
source: None,
|
|
||||||
ctx: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attach or replace the source error (builder-style).
|
|
||||||
pub fn with_source<E>(mut self, source: E) -> Self
|
|
||||||
where
|
|
||||||
E: StdError + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
self.source = Some(Box::new(source));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attach diagnostic context.
|
|
||||||
pub fn with_ctx(mut self, ctx: ErrorContext) -> Self {
|
|
||||||
self.ctx = Some(ctx);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the error classification (builder-style).
|
|
||||||
pub fn with_kind(mut self, kind: BlockErrorKind) -> Self {
|
|
||||||
self.kind = kind;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shorthand: attach an operation name.
|
|
||||||
pub fn with_op(mut self, op: ErrorOp) -> Self {
|
|
||||||
self.ctx.get_or_insert_with(ErrorContext::default).op = Some(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shorthand: attach a file path.
|
|
||||||
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
|
|
||||||
self.ctx.get_or_insert_with(ErrorContext::default).path = Some(path.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shorthand: attach a byte offset.
|
|
||||||
pub fn with_offset(mut self, offset: u64) -> Self {
|
|
||||||
self.ctx.get_or_insert_with(ErrorContext::default).offset = Some(offset);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The error classification.
|
|
||||||
pub fn kind(&self) -> BlockErrorKind {
|
|
||||||
self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The diagnostic context, if any.
|
|
||||||
pub fn context(&self) -> Option<&ErrorContext> {
|
|
||||||
self.ctx.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Access the underlying source error, if any.
|
|
||||||
pub fn source_ref(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
|
|
||||||
self.source.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to downcast the source to a concrete type.
|
|
||||||
pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
|
|
||||||
self.source.as_ref()?.downcast_ref::<T>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume the error and return the boxed source, if any.
|
|
||||||
pub fn into_source(self) -> Option<Box<dyn StdError + Send + Sync + 'static>> {
|
|
||||||
self.source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for BlockError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.kind)?;
|
|
||||||
if let Some(ctx) = &self.ctx {
|
|
||||||
write!(f, " ({ctx})")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StdError for BlockError {
|
|
||||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
|
||||||
self.source
|
|
||||||
.as_ref()
|
|
||||||
.map(|e| e.as_ref() as &(dyn StdError + 'static))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience: wrap an `io::Error` as `BlockErrorKind::Io`.
|
|
||||||
impl From<io::Error> for BlockError {
|
|
||||||
fn from(e: io::Error) -> Self {
|
|
||||||
Self::new(BlockErrorKind::Io, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type BlockResult<T> = Result<T, BlockError>;
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
//! Disk image factory.
|
|
||||||
//!
|
|
||||||
//! [`open_disk`] is the single entry point for opening a disk image.
|
|
||||||
//! It opens the file, detects the image format, probes async I/O
|
|
||||||
//! support, and constructs the appropriate backend. Callers receive
|
|
||||||
//! a trait object that is ready for use by virtio queue workers.
|
|
||||||
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::{fmt, fs};
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
use crate::block_io_uring_is_supported;
|
|
||||||
use crate::disk_file::AsyncFullDiskFile;
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
|
||||||
use crate::fixed_vhd_disk::FixedVhdDisk;
|
|
||||||
use crate::qcow_disk::QcowDisk;
|
|
||||||
use crate::raw_disk::{RawBackend, RawDisk};
|
|
||||||
use crate::vhdx_sync::VhdxDiskSync;
|
|
||||||
use crate::{
|
|
||||||
ImageType, block_aio_is_supported, detect_image_type, open_disk_image, preallocate_disk,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Options for opening a disk image via [`open_disk`].
|
|
||||||
pub struct DiskOpenOptions<'a> {
|
|
||||||
pub path: &'a Path,
|
|
||||||
pub readonly: bool,
|
|
||||||
pub direct: bool,
|
|
||||||
pub sparse: bool,
|
|
||||||
pub backing_files: bool,
|
|
||||||
pub disable_io_uring: bool,
|
|
||||||
pub disable_aio: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result of [`open_disk`], carrying the detected image type alongside
|
|
||||||
/// the constructed backend.
|
|
||||||
pub struct OpenedDisk {
|
|
||||||
pub image_type: ImageType,
|
|
||||||
pub disk: Box<dyn AsyncFullDiskFile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for OpenedDisk {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("OpenedDisk")
|
|
||||||
.field("image_type", &self.image_type)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true when io_uring is supported on the running kernel.
|
|
||||||
///
|
|
||||||
/// The result is cached so the probe runs at most once per process.
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
fn io_uring_supported() -> bool {
|
|
||||||
static SUPPORTED: OnceLock<bool> = OnceLock::new();
|
|
||||||
*SUPPORTED.get_or_init(block_io_uring_is_supported)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true when Linux AIO is supported on the running kernel.
|
|
||||||
///
|
|
||||||
/// The result is cached so the probe runs at most once per process.
|
|
||||||
fn aio_supported() -> bool {
|
|
||||||
static SUPPORTED: OnceLock<bool> = OnceLock::new();
|
|
||||||
*SUPPORTED.get_or_init(block_aio_is_supported)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open a disk image and construct the appropriate async backend.
|
|
||||||
///
|
|
||||||
/// - Opens the file with the requested access mode and flags.
|
|
||||||
/// - Detects the image format from the file header.
|
|
||||||
/// - Probes io_uring and Linux AIO support on the running kernel.
|
|
||||||
/// - Constructs the most capable backend available for the detected
|
|
||||||
/// format, preferring io_uring over AIO over synchronous fallback.
|
|
||||||
///
|
|
||||||
/// The returned [`OpenedDisk`] exposes the detected [`ImageType`] so
|
|
||||||
/// callers can perform post construction validation (e.g. type mismatch
|
|
||||||
/// checks, configuration warnings).
|
|
||||||
pub fn open_disk(options: &DiskOpenOptions<'_>) -> BlockResult<OpenedDisk> {
|
|
||||||
let mut fs_options = fs::OpenOptions::new();
|
|
||||||
fs_options.read(true);
|
|
||||||
fs_options.write(!options.readonly);
|
|
||||||
if options.direct {
|
|
||||||
fs_options.custom_flags(libc::O_DIRECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = open_disk_image(options.path, &fs_options)?;
|
|
||||||
let image_type = detect_image_type(&mut file)?;
|
|
||||||
|
|
||||||
let disk: Box<dyn AsyncFullDiskFile> = match image_type {
|
|
||||||
ImageType::FixedVhd => open_fixed_vhd(file, options)?,
|
|
||||||
ImageType::Raw => open_raw(file, options)?,
|
|
||||||
ImageType::Qcow2 => open_qcow2(file, options)?,
|
|
||||||
ImageType::Vhdx => open_vhdx(file, options)?,
|
|
||||||
ImageType::Unknown => {
|
|
||||||
return Err(
|
|
||||||
BlockError::from_kind(BlockErrorKind::UnsupportedFeature).with_path(options.path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(OpenedDisk { image_type, disk })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_vhdx(
|
|
||||||
file: fs::File,
|
|
||||||
options: &DiskOpenOptions<'_>,
|
|
||||||
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
|
|
||||||
info!("Opening VHDX disk file with synchronous backend");
|
|
||||||
Ok(Box::new(
|
|
||||||
VhdxDiskSync::new(file).map_err(|e| e.with_path(options.path))?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_fixed_vhd(
|
|
||||||
file: fs::File,
|
|
||||||
options: &DiskOpenOptions<'_>,
|
|
||||||
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
if !options.disable_io_uring {
|
|
||||||
if io_uring_supported() {
|
|
||||||
info!("Opening fixed VHD disk file with io_uring backend");
|
|
||||||
return Ok(Box::new(
|
|
||||||
FixedVhdDisk::new(file, true).map_err(|e| e.with_path(options.path))?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
info!("io_uring runtime probe failed for fixed VHD, using synchronous backend");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Opening fixed VHD disk file with synchronous backend");
|
|
||||||
Ok(Box::new(
|
|
||||||
FixedVhdDisk::new(file, false).map_err(|e| e.with_path(options.path))?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_raw(
|
|
||||||
file: fs::File,
|
|
||||||
options: &DiskOpenOptions<'_>,
|
|
||||||
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
|
|
||||||
if !options.readonly && !options.sparse {
|
|
||||||
preallocate_disk(&file, options.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
if !options.disable_io_uring {
|
|
||||||
if io_uring_supported() {
|
|
||||||
info!("Opening RAW disk file with io_uring backend");
|
|
||||||
return Ok(Box::new(RawDisk::new(file, RawBackend::IoUring)));
|
|
||||||
}
|
|
||||||
info!("io_uring runtime probe failed for RAW, trying next backend");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !options.disable_aio {
|
|
||||||
if aio_supported() {
|
|
||||||
info!("Opening RAW disk file with AIO backend");
|
|
||||||
return Ok(Box::new(RawDisk::new(file, RawBackend::Aio)));
|
|
||||||
}
|
|
||||||
info!("AIO runtime probe failed for RAW, using synchronous backend");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Opening RAW disk file with synchronous backend");
|
|
||||||
Ok(Box::new(RawDisk::new(file, RawBackend::Sync)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_qcow2(
|
|
||||||
file: fs::File,
|
|
||||||
options: &DiskOpenOptions<'_>,
|
|
||||||
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
if !options.disable_io_uring {
|
|
||||||
if io_uring_supported() {
|
|
||||||
info!("Opening QCOW2 disk file with io_uring backend");
|
|
||||||
return Ok(Box::new(
|
|
||||||
QcowDisk::new(
|
|
||||||
file,
|
|
||||||
options.direct,
|
|
||||||
options.backing_files,
|
|
||||||
options.sparse,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.map_err(|e| e.with_path(options.path))?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
info!("io_uring runtime probe failed for QCOW2, using synchronous backend");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Opening QCOW2 disk file with synchronous backend");
|
|
||||||
Ok(Box::new(
|
|
||||||
QcowDisk::new(
|
|
||||||
file,
|
|
||||||
options.direct,
|
|
||||||
options.backing_files,
|
|
||||||
options.sparse,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.map_err(|e| e.with_path(options.path))?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::qcow::{QcowFile, RawFile};
|
|
||||||
|
|
||||||
fn default_options(path: &Path) -> DiskOpenOptions<'_> {
|
|
||||||
DiskOpenOptions {
|
|
||||||
path,
|
|
||||||
readonly: false,
|
|
||||||
direct: false,
|
|
||||||
sparse: false,
|
|
||||||
backing_files: false,
|
|
||||||
disable_io_uring: true,
|
|
||||||
disable_aio: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nonexistent_path_returns_error() {
|
|
||||||
let path = Path::new("/tmp/no_such_disk_image.raw");
|
|
||||||
let options = default_options(path);
|
|
||||||
match open_disk(&options) {
|
|
||||||
Err(e) => assert_eq!(e.kind(), BlockErrorKind::Io),
|
|
||||||
Ok(_) => panic!("expected error for nonexistent path"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn detect_raw_image() {
|
|
||||||
let tmp = TempFile::new().unwrap();
|
|
||||||
tmp.as_file().set_len(1 << 20).unwrap();
|
|
||||||
let path = tmp.as_path().to_owned();
|
|
||||||
let options = default_options(&path);
|
|
||||||
let opened = open_disk(&options).unwrap();
|
|
||||||
assert_eq!(opened.image_type, ImageType::Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn detect_qcow2_image() {
|
|
||||||
let tmp = TempFile::new().unwrap();
|
|
||||||
{
|
|
||||||
let raw = RawFile::new(tmp.as_file().try_clone().unwrap(), false);
|
|
||||||
let mut qcow = QcowFile::new(raw, 3, 100 * 1024 * 1024, true).unwrap();
|
|
||||||
qcow.flush().unwrap();
|
|
||||||
}
|
|
||||||
let path = tmp.as_path().to_owned();
|
|
||||||
let options = default_options(&path);
|
|
||||||
let opened = open_disk(&options).unwrap();
|
|
||||||
assert_eq!(opened.image_type, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_readonly() {
|
|
||||||
let tmp = TempFile::new().unwrap();
|
|
||||||
tmp.as_file().set_len(1 << 20).unwrap();
|
|
||||||
let path = tmp.as_path().to_owned();
|
|
||||||
let mut options = default_options(&path);
|
|
||||||
options.readonly = true;
|
|
||||||
let opened = open_disk(&options).unwrap();
|
|
||||||
assert_eq!(opened.image_type, ImageType::Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_fallback_when_async_disabled() {
|
|
||||||
let tmp = TempFile::new().unwrap();
|
|
||||||
let size = 1u64 << 20;
|
|
||||||
tmp.as_file().set_len(size).unwrap();
|
|
||||||
let path = tmp.as_path().to_owned();
|
|
||||||
let options = DiskOpenOptions {
|
|
||||||
path: &path,
|
|
||||||
readonly: false,
|
|
||||||
direct: false,
|
|
||||||
sparse: false,
|
|
||||||
backing_files: false,
|
|
||||||
disable_io_uring: true,
|
|
||||||
disable_aio: true,
|
|
||||||
};
|
|
||||||
let opened = open_disk(&options).unwrap();
|
|
||||||
assert_eq!(opened.image_type, ImageType::Raw);
|
|
||||||
assert_eq!(opened.disk.logical_size().unwrap(), size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, RawFd};
|
use std::os::fd::{AsRawFd, RawFd};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -141,37 +140,6 @@ impl LockGranularity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User-facing choice for the lock granularity.
|
|
||||||
///
|
|
||||||
/// This allows external management software to create snapshots of the disk
|
|
||||||
/// image. Without a byte-range lock, some NFS implementations may treat the
|
|
||||||
/// entire file as exclusively locked and prevent such operations (e.g. NetApp).
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum LockGranularityChoice {
|
|
||||||
/// Byte-range lock covering [0, size).
|
|
||||||
#[default]
|
|
||||||
ByteRange,
|
|
||||||
/// Whole-file lock (l_start=0, l_len=0) - original OFD whole-file lock behavior.
|
|
||||||
Full,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error returned when parsing a [`LockGranularityChoice`] from a string.
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
#[error("Invalid lock granularity value: {0}, expected 'byte-range' or 'full'")]
|
|
||||||
pub struct LockGranularityParseError(String);
|
|
||||||
|
|
||||||
impl FromStr for LockGranularityChoice {
|
|
||||||
type Err = LockGranularityParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"byte-range" => Ok(LockGranularityChoice::ByteRange),
|
|
||||||
"full" => Ok(LockGranularityChoice::Full),
|
|
||||||
_ => Err(LockGranularityParseError(s.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [`struct@libc::flock`] structure for the whole file.
|
/// Returns a [`struct@libc::flock`] structure for the whole file.
|
||||||
const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> libc::flock {
|
const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> libc::flock {
|
||||||
libc::flock {
|
libc::flock {
|
||||||
|
|||||||
@@ -2,14 +2,50 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::os::unix::io::RawFd;
|
use std::fs::File;
|
||||||
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::BatchRequest;
|
use crate::async_io::{
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
use crate::error::BlockResult;
|
};
|
||||||
|
use crate::fixed_vhd::FixedVhd;
|
||||||
use crate::raw_async::RawFileAsync;
|
use crate::raw_async::RawFileAsync;
|
||||||
|
use crate::{BatchRequest, BlockBackend};
|
||||||
|
|
||||||
|
pub struct FixedVhdDiskAsync(FixedVhd);
|
||||||
|
|
||||||
|
impl FixedVhdDiskAsync {
|
||||||
|
pub fn new(file: File) -> std::io::Result<Self> {
|
||||||
|
Ok(Self(FixedVhd::new(file)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskFile for FixedVhdDiskAsync {
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
Ok(self.0.logical_size().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
Ok(self.0.physical_size().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
FixedVhdAsync::new(
|
||||||
|
self.0.as_raw_fd(),
|
||||||
|
ring_depth,
|
||||||
|
self.0.logical_size().unwrap(),
|
||||||
|
)
|
||||||
|
.map_err(DiskFileError::NewAsyncIo)?,
|
||||||
|
) as Box<dyn AsyncIo>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
|
BorrowedDiskFd::new(self.0.as_raw_fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FixedVhdAsync {
|
pub struct FixedVhdAsync {
|
||||||
raw_file_async: RawFileAsync,
|
raw_file_async: RawFileAsync,
|
||||||
@@ -17,7 +53,7 @@ pub struct FixedVhdAsync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FixedVhdAsync {
|
impl FixedVhdAsync {
|
||||||
pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> BlockResult<Self> {
|
pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> std::io::Result<Self> {
|
||||||
let raw_file_async = RawFileAsync::new(fd, ring_depth)?;
|
let raw_file_async = RawFileAsync::new(fd, ring_depth)?;
|
||||||
|
|
||||||
Ok(FixedVhdAsync {
|
Ok(FixedVhdAsync {
|
||||||
@@ -79,18 +115,6 @@ impl AsyncIo for FixedVhdAsync {
|
|||||||
self.raw_file_async.next_completed_request()
|
self.raw_file_async.next_completed_request()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::PunchHole(std::io::Error::other(
|
|
||||||
"punch_hole not supported for fixed VHD",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
|
|
||||||
"write_zeroes not supported for fixed VHD",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn batch_requests_enabled(&self) -> bool {
|
fn batch_requests_enabled(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
|
|
||||||
use crate::disk_file::DiskSize;
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
|
||||||
use crate::fixed_vhd::FixedVhd;
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
use crate::fixed_vhd_async::FixedVhdAsync;
|
|
||||||
use crate::fixed_vhd_sync::FixedVhdSync;
|
|
||||||
use crate::{BlockBackend, Error, disk_file};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FixedVhdDisk {
|
|
||||||
inner: FixedVhd,
|
|
||||||
use_io_uring: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FixedVhdDisk {
|
|
||||||
pub fn new(file: File, use_io_uring: bool) -> BlockResult<Self> {
|
|
||||||
#[cfg(not(feature = "io_uring"))]
|
|
||||||
if use_io_uring {
|
|
||||||
return Err(BlockError::new(
|
|
||||||
BlockErrorKind::UnsupportedFeature,
|
|
||||||
DiskFileError::NewAsyncIo(io::Error::other(
|
|
||||||
"io_uring requested but feature is not enabled",
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
inner: FixedVhd::new(file).map_err(|e| BlockError::from(e).with_op(ErrorOp::Open))?,
|
|
||||||
use_io_uring,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskSize for FixedVhdDisk {
|
|
||||||
fn logical_size(&self) -> BlockResult<u64> {
|
|
||||||
self.inner
|
|
||||||
.logical_size()
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::PhysicalSize for FixedVhdDisk {
|
|
||||||
fn physical_size(&self) -> BlockResult<u64> {
|
|
||||||
self.inner.physical_size().map_err(|e| match e {
|
|
||||||
Error::GetFileMetadata(io) => {
|
|
||||||
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
|
|
||||||
}
|
|
||||||
_ => unreachable!("unexpected error from FixedVhd::physical_size(): {e}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFd for FixedVhdDisk {
|
|
||||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
|
||||||
BorrowedDiskFd::new(self.inner.as_raw_fd())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Geometry for FixedVhdDisk {}
|
|
||||||
|
|
||||||
impl disk_file::SparseCapable for FixedVhdDisk {}
|
|
||||||
|
|
||||||
impl disk_file::Resizable for FixedVhdDisk {
|
|
||||||
fn resize(&mut self, _size: u64) -> BlockResult<()> {
|
|
||||||
Err(BlockError::new(
|
|
||||||
BlockErrorKind::UnsupportedFeature,
|
|
||||||
DiskFileError::ResizeError(io::Error::other("resize not supported for fixed VHD")),
|
|
||||||
)
|
|
||||||
.with_op(ErrorOp::Resize))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFile for FixedVhdDisk {}
|
|
||||||
|
|
||||||
impl disk_file::AsyncDiskFile for FixedVhdDisk {
|
|
||||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
|
||||||
Ok(Box::new(FixedVhdDisk {
|
|
||||||
inner: self.inner.clone(),
|
|
||||||
use_io_uring: self.use_io_uring,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
|
||||||
let size = self.logical_size()?;
|
|
||||||
|
|
||||||
if self.use_io_uring {
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
{
|
|
||||||
return Ok(Box::new(FixedVhdAsync::new(
|
|
||||||
self.inner.as_raw_fd(),
|
|
||||||
ring_depth,
|
|
||||||
size,
|
|
||||||
)?));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "io_uring"))]
|
|
||||||
unreachable!("use_io_uring is set but io_uring feature is not enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = ring_depth;
|
|
||||||
Ok(Box::new(
|
|
||||||
FixedVhdSync::new(self.inner.as_raw_fd(), size).map_err(|e| {
|
|
||||||
BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e))
|
|
||||||
.with_op(ErrorOp::Open)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::async_io::AsyncIo;
|
|
||||||
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable};
|
|
||||||
|
|
||||||
/// Minimal fixed VHD footer (disk type = 2, current_size = 0x11223344).
|
|
||||||
fn fixed_vhd_footer() -> &'static [u8] {
|
|
||||||
&[
|
|
||||||
0x63, 0x6f, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x78, // cookie
|
|
||||||
0x00, 0x00, 0x00, 0x02, // features
|
|
||||||
0x00, 0x01, 0x00, 0x00, // file format version
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // data offset
|
|
||||||
0x27, 0xa6, 0xa6, 0x5d, // time stamp
|
|
||||||
0x71, 0x65, 0x6d, 0x75, // creator application
|
|
||||||
0x00, 0x05, 0x00, 0x03, // creator version
|
|
||||||
0x57, 0x69, 0x32, 0x6b, // creator host os
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // original size
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // current size
|
|
||||||
0x11, 0xe0, 0x10, 0x3f, // disk geometry
|
|
||||||
0x00, 0x00, 0x00, 0x02, // disk type
|
|
||||||
0x00, 0x00, 0x00, 0x00, // checksum
|
|
||||||
0x98, 0x7b, 0xb1, 0xcd, 0x84, 0x14, 0x41, 0xfc, // unique id
|
|
||||||
0xa4, 0xab, 0xd0, 0x69, 0x45, 0x2b, 0xf2, 0x23, 0x00, // saved state
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_vhd_file() -> File {
|
|
||||||
let mut file: File = TempFile::new().unwrap().into_file();
|
|
||||||
let data_size: u64 = 0x1122_3344;
|
|
||||||
file.set_len(data_size + 0x200).unwrap();
|
|
||||||
file.seek(SeekFrom::Start(data_size)).unwrap();
|
|
||||||
file.write_all(fixed_vhd_footer()).unwrap();
|
|
||||||
file
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_sync_returns_correct_size() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, false).unwrap();
|
|
||||||
assert_eq!(disk.logical_size().unwrap(), 0x1122_3344);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) {
|
|
||||||
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
|
|
||||||
assert_eq!(io.batch_requests_enabled(), expect_batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_async_io(disk: &FixedVhdDisk, expect_batch: bool) {
|
|
||||||
assert_async_io_from_dyn(disk, expect_batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_backend_disables_batch_requests() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, false).unwrap();
|
|
||||||
assert_async_io(&disk, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn io_uring_backend_enables_batch_requests() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, true).unwrap();
|
|
||||||
assert_async_io(&disk, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_sync_dispatch() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, false).unwrap();
|
|
||||||
let cloned = disk.try_clone().unwrap();
|
|
||||||
assert_async_io_from_dyn(cloned.as_ref(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_io_uring_dispatch() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, true).unwrap();
|
|
||||||
let cloned = disk.try_clone().unwrap();
|
|
||||||
assert_async_io_from_dyn(cloned.as_ref(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resize_returns_error() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let mut disk = FixedVhdDisk::new(file, false).unwrap();
|
|
||||||
assert!(disk.resize(0x2000_0000).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn physical_size_includes_footer() {
|
|
||||||
let file = make_vhd_file();
|
|
||||||
let disk = FixedVhdDisk::new(file, false).unwrap();
|
|
||||||
// Data region (0x1122_3344) + VHD footer (0x200).
|
|
||||||
assert_eq!(disk.physical_size().unwrap(), 0x1122_3344 + 0x200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,53 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::os::unix::io::RawFd;
|
use std::fs::File;
|
||||||
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
use crate::BlockBackend;
|
||||||
|
use crate::async_io::{
|
||||||
|
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
|
};
|
||||||
|
use crate::fixed_vhd::FixedVhd;
|
||||||
use crate::raw_sync::RawFileSync;
|
use crate::raw_sync::RawFileSync;
|
||||||
|
|
||||||
|
pub struct FixedVhdDiskSync(FixedVhd);
|
||||||
|
|
||||||
|
impl FixedVhdDiskSync {
|
||||||
|
pub fn new(file: File) -> std::io::Result<Self> {
|
||||||
|
Ok(Self(FixedVhd::new(file)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskFile for FixedVhdDiskSync {
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
Ok(self.0.logical_size().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.0.physical_size().map_err(|e| {
|
||||||
|
let io_inner = match e {
|
||||||
|
crate::Error::GetFileMetadata(e) => e,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
DiskFileError::Size(io_inner)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
FixedVhdSync::new(self.0.as_raw_fd(), self.0.logical_size().unwrap())
|
||||||
|
.map_err(DiskFileError::NewAsyncIo)?,
|
||||||
|
) as Box<dyn AsyncIo>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
|
BorrowedDiskFd::new(self.0.as_raw_fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FixedVhdSync {
|
pub struct FixedVhdSync {
|
||||||
raw_file_sync: RawFileSync,
|
raw_file_sync: RawFileSync,
|
||||||
size: u64,
|
size: u64,
|
||||||
@@ -73,16 +113,4 @@ impl AsyncIo for FixedVhdSync {
|
|||||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||||
self.raw_file_sync.next_completed_request()
|
self.raw_file_sync.next_completed_request()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::PunchHole(std::io::Error::other(
|
|
||||||
"punch_hole not supported for fixed VHD",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
|
|
||||||
"write_zeroes not supported for fixed VHD",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
866
block/src/lib.rs
866
block/src/lib.rs
@@ -8,70 +8,61 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||||
|
|
||||||
mod aligned_operation;
|
|
||||||
pub mod async_io;
|
pub mod async_io;
|
||||||
pub mod disk_file;
|
|
||||||
pub mod error;
|
|
||||||
pub mod factory;
|
|
||||||
pub mod fcntl;
|
pub mod fcntl;
|
||||||
pub mod fixed_vhd;
|
pub mod fixed_vhd;
|
||||||
#[cfg(feature = "io_uring")]
|
#[cfg(feature = "io_uring")]
|
||||||
/// Enabled with the `"io_uring"` feature
|
/// Enabled with the `"io_uring"` feature
|
||||||
pub mod fixed_vhd_async;
|
pub mod fixed_vhd_async;
|
||||||
pub mod fixed_vhd_disk;
|
|
||||||
pub mod fixed_vhd_sync;
|
pub mod fixed_vhd_sync;
|
||||||
pub mod qcow;
|
pub mod qcow;
|
||||||
|
pub mod qcow_sync;
|
||||||
#[cfg(feature = "io_uring")]
|
#[cfg(feature = "io_uring")]
|
||||||
pub(crate) mod qcow_async;
|
/// Async primitives based on `io-uring`
|
||||||
pub(crate) mod qcow_common;
|
///
|
||||||
pub mod qcow_disk;
|
/// Enabled with the `"io_uring"` feature
|
||||||
pub(crate) mod qcow_sync;
|
pub mod raw_async;
|
||||||
#[cfg(feature = "io_uring")]
|
pub mod raw_async_aio;
|
||||||
pub(crate) mod raw_async;
|
pub mod raw_sync;
|
||||||
pub(crate) mod raw_async_aio;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod raw_async_io_tests;
|
|
||||||
pub mod raw_disk;
|
|
||||||
pub(crate) mod raw_sync;
|
|
||||||
mod request;
|
|
||||||
pub mod vhd;
|
pub mod vhd;
|
||||||
pub mod vhdx;
|
pub mod vhdx;
|
||||||
pub mod vhdx_sync;
|
pub mod vhdx_sync;
|
||||||
|
|
||||||
use std::alloc::{Layout, alloc_zeroed};
|
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::File;
|
||||||
use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
|
use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
|
||||||
use std::os::linux::fs::MetadataExt;
|
use std::os::linux::fs::MetadataExt;
|
||||||
use std::os::unix::fs::FileTypeExt;
|
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{cmp, mem, result};
|
use std::time::Instant;
|
||||||
|
use std::{cmp, result};
|
||||||
|
|
||||||
pub use aligned_operation::AlignedOperation;
|
|
||||||
#[cfg(feature = "io_uring")]
|
#[cfg(feature = "io_uring")]
|
||||||
use io_uring::{IoUring, Probe, opcode};
|
use io_uring::{IoUring, Probe, opcode};
|
||||||
use libc::{
|
use libc::{S_IFBLK, S_IFMT, ioctl};
|
||||||
FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, S_IFBLK, S_IFMT, ioctl,
|
use log::{error, info, warn};
|
||||||
};
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
pub use request::{BatchRequest, ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use virtio_bindings::virtio_blk::*;
|
use virtio_bindings::virtio_blk::*;
|
||||||
|
use virtio_queue::DescriptorChain;
|
||||||
use vm_memory::bitmap::Bitmap;
|
use vm_memory::bitmap::Bitmap;
|
||||||
use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError};
|
use vm_memory::{
|
||||||
|
ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryLoadGuard,
|
||||||
|
};
|
||||||
|
use vm_virtio::{AccessPlatform, Translatable};
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
use vmm_sys_util::{aio, ioctl_io_nr, ioctl_ior_nr};
|
use vmm_sys_util::{aio, ioctl_io_nr};
|
||||||
|
|
||||||
use crate::async_io::{AsyncIoError, AsyncIoResult};
|
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
|
||||||
use crate::request::{DEFAULT_DESCRIPTOR_VEC_SIZE, SECTOR_SIZE};
|
|
||||||
use crate::vhdx::VhdxError;
|
use crate::vhdx::VhdxError;
|
||||||
|
|
||||||
|
const SECTOR_SHIFT: u8 = 9;
|
||||||
|
pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Guest gave us bad memory addresses")]
|
#[error("Guest gave us bad memory addresses")]
|
||||||
@@ -100,8 +91,6 @@ pub enum Error {
|
|||||||
RawFileError(#[source] std::io::Error),
|
RawFileError(#[source] std::io::Error),
|
||||||
#[error("The requested operation does not support multiple descriptors")]
|
#[error("The requested operation does not support multiple descriptors")]
|
||||||
TooManyDescriptors,
|
TooManyDescriptors,
|
||||||
#[error("Request contains too many segments ({0}, max {MAX_DISCARD_WRITE_ZEROES_SEG})")]
|
|
||||||
TooManySegments(u32),
|
|
||||||
#[error("Failure in vhdx")]
|
#[error("Failure in vhdx")]
|
||||||
VhdxError(#[source] VhdxError),
|
VhdxError(#[source] VhdxError),
|
||||||
}
|
}
|
||||||
@@ -158,8 +147,6 @@ pub enum ExecuteError {
|
|||||||
WriteAll(#[source] io::Error),
|
WriteAll(#[source] io::Error),
|
||||||
#[error("Unsupported request: {0}")]
|
#[error("Unsupported request: {0}")]
|
||||||
Unsupported(u32),
|
Unsupported(u32),
|
||||||
#[error("Unsupported flags {flags:#x} for request type {request_type}")]
|
|
||||||
UnsupportedFlags { request_type: u32, flags: u32 },
|
|
||||||
#[error("Failed to submit io uring")]
|
#[error("Failed to submit io uring")]
|
||||||
SubmitIoUring(#[source] io::Error),
|
SubmitIoUring(#[source] io::Error),
|
||||||
#[error("Failed to get guest address")]
|
#[error("Failed to get guest address")]
|
||||||
@@ -170,10 +157,6 @@ pub enum ExecuteError {
|
|||||||
AsyncWrite(#[source] AsyncIoError),
|
AsyncWrite(#[source] AsyncIoError),
|
||||||
#[error("failed to async flush")]
|
#[error("failed to async flush")]
|
||||||
AsyncFlush(#[source] AsyncIoError),
|
AsyncFlush(#[source] AsyncIoError),
|
||||||
#[error("Failed to async punch hole")]
|
|
||||||
AsyncPunchHole(#[source] AsyncIoError),
|
|
||||||
#[error("Failed to async write zeroes")]
|
|
||||||
AsyncWriteZeroes(#[source] AsyncIoError),
|
|
||||||
#[error("Failed allocating a temporary buffer")]
|
#[error("Failed allocating a temporary buffer")]
|
||||||
TemporaryBufferAllocation(#[source] io::Error),
|
TemporaryBufferAllocation(#[source] io::Error),
|
||||||
}
|
}
|
||||||
@@ -190,20 +173,26 @@ impl ExecuteError {
|
|||||||
ExecuteError::Write(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::Write(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::WriteAll(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::WriteAll(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::Unsupported(_) => VIRTIO_BLK_S_UNSUPP,
|
ExecuteError::Unsupported(_) => VIRTIO_BLK_S_UNSUPP,
|
||||||
ExecuteError::UnsupportedFlags { .. } => VIRTIO_BLK_S_UNSUPP,
|
|
||||||
ExecuteError::SubmitIoUring(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::SubmitIoUring(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::GetHostAddress(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::GetHostAddress(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::AsyncRead(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::AsyncRead(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::AsyncWrite(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::AsyncWrite(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::AsyncFlush(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::AsyncFlush(_) => VIRTIO_BLK_S_IOERR,
|
||||||
ExecuteError::AsyncPunchHole(_) => VIRTIO_BLK_S_IOERR,
|
|
||||||
ExecuteError::AsyncWriteZeroes(_) => VIRTIO_BLK_S_IOERR,
|
|
||||||
ExecuteError::TemporaryBufferAllocation(_) => VIRTIO_BLK_S_IOERR,
|
ExecuteError::TemporaryBufferAllocation(_) => VIRTIO_BLK_S_IOERR,
|
||||||
};
|
};
|
||||||
status as u8
|
status as u8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum RequestType {
|
||||||
|
In,
|
||||||
|
Out,
|
||||||
|
Flush,
|
||||||
|
GetDeviceId,
|
||||||
|
Unsupported(u32),
|
||||||
|
}
|
||||||
|
|
||||||
pub fn request_type<B: Bitmap + 'static>(
|
pub fn request_type<B: Bitmap + 'static>(
|
||||||
mem: &vm_memory::GuestMemoryMmap<B>,
|
mem: &vm_memory::GuestMemoryMmap<B>,
|
||||||
desc_addr: GuestAddress,
|
desc_addr: GuestAddress,
|
||||||
@@ -214,8 +203,6 @@ pub fn request_type<B: Bitmap + 'static>(
|
|||||||
VIRTIO_BLK_T_OUT => Ok(RequestType::Out),
|
VIRTIO_BLK_T_OUT => Ok(RequestType::Out),
|
||||||
VIRTIO_BLK_T_FLUSH => Ok(RequestType::Flush),
|
VIRTIO_BLK_T_FLUSH => Ok(RequestType::Flush),
|
||||||
VIRTIO_BLK_T_GET_ID => Ok(RequestType::GetDeviceId),
|
VIRTIO_BLK_T_GET_ID => Ok(RequestType::GetDeviceId),
|
||||||
VIRTIO_BLK_T_DISCARD => Ok(RequestType::Discard),
|
|
||||||
VIRTIO_BLK_T_WRITE_ZEROES => Ok(RequestType::WriteZeroes),
|
|
||||||
t => Ok(RequestType::Unsupported(t)),
|
t => Ok(RequestType::Unsupported(t)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +220,372 @@ fn sector<B: Bitmap + 'static>(
|
|||||||
mem.read_obj(addr).map_err(Error::GuestMemory)
|
mem.read_obj(addr).map_err(Error::GuestMemory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AlignedOperation {
|
||||||
|
origin_ptr: u64,
|
||||||
|
aligned_ptr: u64,
|
||||||
|
size: usize,
|
||||||
|
layout: Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BatchRequest {
|
||||||
|
pub offset: libc::off_t,
|
||||||
|
pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
||||||
|
pub user_data: u64,
|
||||||
|
pub request_type: RequestType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExecuteAsync {
|
||||||
|
// `true` if the execution will complete asynchronously
|
||||||
|
pub async_complete: bool,
|
||||||
|
// request need to be batched for submission if any
|
||||||
|
pub batch_request: Option<BatchRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Request {
|
||||||
|
pub request_type: RequestType,
|
||||||
|
pub sector: u64,
|
||||||
|
pub data_descriptors: SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
||||||
|
pub status_addr: GuestAddress,
|
||||||
|
pub writeback: bool,
|
||||||
|
pub aligned_operations: SmallVec<[AlignedOperation; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
||||||
|
pub start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn parse<B: Bitmap + 'static>(
|
||||||
|
desc_chain: &mut DescriptorChain<GuestMemoryLoadGuard<vm_memory::GuestMemoryMmap<B>>>,
|
||||||
|
access_platform: Option<&dyn AccessPlatform>,
|
||||||
|
) -> result::Result<Request, Error> {
|
||||||
|
let hdr_desc = desc_chain
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::DescriptorChainTooShort)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
error!("Missing head descriptor");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// The head contains the request type which MUST be readable.
|
||||||
|
if hdr_desc.is_write_only() {
|
||||||
|
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hdr_desc_addr = hdr_desc
|
||||||
|
.addr()
|
||||||
|
.translate_gva(access_platform, hdr_desc.len() as usize);
|
||||||
|
|
||||||
|
let mut req = Request {
|
||||||
|
request_type: request_type(desc_chain.memory(), hdr_desc_addr)?,
|
||||||
|
sector: sector(desc_chain.memory(), hdr_desc_addr)?,
|
||||||
|
data_descriptors: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
|
||||||
|
status_addr: GuestAddress(0),
|
||||||
|
writeback: true,
|
||||||
|
aligned_operations: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
|
||||||
|
start: Instant::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_desc;
|
||||||
|
let mut desc = desc_chain
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::DescriptorChainTooShort)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
error!("Only head descriptor present: request = {req:?}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if desc.has_next() {
|
||||||
|
req.data_descriptors.reserve_exact(1);
|
||||||
|
while desc.has_next() {
|
||||||
|
if desc.is_write_only() && req.request_type == RequestType::Out {
|
||||||
|
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
||||||
|
}
|
||||||
|
if !desc.is_write_only() && req.request_type == RequestType::In {
|
||||||
|
return Err(Error::UnexpectedReadOnlyDescriptor);
|
||||||
|
}
|
||||||
|
if !desc.is_write_only() && req.request_type == RequestType::GetDeviceId {
|
||||||
|
return Err(Error::UnexpectedReadOnlyDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.data_descriptors.push((
|
||||||
|
desc.addr()
|
||||||
|
.translate_gva(access_platform, desc.len() as usize),
|
||||||
|
desc.len(),
|
||||||
|
));
|
||||||
|
desc = desc_chain
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::DescriptorChainTooShort)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
error!("DescriptorChain corrupted: request = {req:?}");
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
status_desc = desc;
|
||||||
|
} else {
|
||||||
|
status_desc = desc;
|
||||||
|
// Only flush requests are allowed to skip the data descriptor.
|
||||||
|
if req.request_type != RequestType::Flush {
|
||||||
|
error!("Need a data descriptor: request = {req:?}");
|
||||||
|
return Err(Error::DescriptorChainTooShort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The status MUST always be writable.
|
||||||
|
if !status_desc.is_write_only() {
|
||||||
|
return Err(Error::UnexpectedReadOnlyDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if status_desc.len() < 1 {
|
||||||
|
return Err(Error::DescriptorLengthTooSmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.status_addr = status_desc
|
||||||
|
.addr()
|
||||||
|
.translate_gva(access_platform, status_desc.len() as usize);
|
||||||
|
|
||||||
|
Ok(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute<T: Seek + Read + Write, B: Bitmap + 'static>(
|
||||||
|
&self,
|
||||||
|
disk: &mut T,
|
||||||
|
disk_nsectors: u64,
|
||||||
|
mem: &vm_memory::GuestMemoryMmap<B>,
|
||||||
|
serial: &[u8],
|
||||||
|
) -> result::Result<u32, ExecuteError> {
|
||||||
|
disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT))
|
||||||
|
.map_err(ExecuteError::Seek)?;
|
||||||
|
let mut len = 0;
|
||||||
|
for (data_addr, data_len) in &self.data_descriptors {
|
||||||
|
let mut top: u64 = u64::from(*data_len) / SECTOR_SIZE;
|
||||||
|
if u64::from(*data_len) % SECTOR_SIZE != 0 {
|
||||||
|
top += 1;
|
||||||
|
}
|
||||||
|
top = top
|
||||||
|
.checked_add(self.sector)
|
||||||
|
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
||||||
|
if top > disk_nsectors {
|
||||||
|
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.request_type {
|
||||||
|
RequestType::In => {
|
||||||
|
let mut buf = vec![0u8; *data_len as usize];
|
||||||
|
disk.read_exact(&mut buf).map_err(ExecuteError::ReadExact)?;
|
||||||
|
mem.read_exact_volatile_from(
|
||||||
|
*data_addr,
|
||||||
|
&mut buf.as_slice(),
|
||||||
|
*data_len as usize,
|
||||||
|
)
|
||||||
|
.map_err(ExecuteError::Read)?;
|
||||||
|
len += data_len;
|
||||||
|
}
|
||||||
|
RequestType::Out => {
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize)
|
||||||
|
.map_err(ExecuteError::Write)?;
|
||||||
|
disk.write_all(&buf).map_err(ExecuteError::WriteAll)?;
|
||||||
|
if !self.writeback {
|
||||||
|
disk.flush().map_err(ExecuteError::Flush)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequestType::Flush => disk.flush().map_err(ExecuteError::Flush)?,
|
||||||
|
RequestType::GetDeviceId => {
|
||||||
|
if (*data_len as usize) < serial.len() {
|
||||||
|
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
||||||
|
}
|
||||||
|
mem.write_slice(serial, *data_addr)
|
||||||
|
.map_err(ExecuteError::Write)?;
|
||||||
|
}
|
||||||
|
RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_async<B: Bitmap + 'static>(
|
||||||
|
&mut self,
|
||||||
|
mem: &vm_memory::GuestMemoryMmap<B>,
|
||||||
|
disk_nsectors: u64,
|
||||||
|
disk_image: &mut dyn AsyncIo,
|
||||||
|
serial: &[u8],
|
||||||
|
user_data: u64,
|
||||||
|
) -> result::Result<ExecuteAsync, ExecuteError> {
|
||||||
|
let sector = self.sector;
|
||||||
|
let request_type = self.request_type;
|
||||||
|
let offset = (sector << SECTOR_SHIFT) as libc::off_t;
|
||||||
|
|
||||||
|
let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
|
||||||
|
SmallVec::with_capacity(self.data_descriptors.len());
|
||||||
|
for &(data_addr, data_len) in &self.data_descriptors {
|
||||||
|
let _: u32 = data_len; // compiler-checked documentation
|
||||||
|
const _: () = assert!(
|
||||||
|
core::mem::size_of::<u32>() <= core::mem::size_of::<usize>(),
|
||||||
|
"unsupported platform"
|
||||||
|
);
|
||||||
|
if data_len == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut top: u64 = u64::from(data_len) / SECTOR_SIZE;
|
||||||
|
if u64::from(data_len) % SECTOR_SIZE != 0 {
|
||||||
|
top += 1;
|
||||||
|
}
|
||||||
|
let data_len = data_len as usize;
|
||||||
|
top = top
|
||||||
|
.checked_add(sector)
|
||||||
|
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
||||||
|
if top > disk_nsectors {
|
||||||
|
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
let origin_ptr = mem
|
||||||
|
.get_slice(data_addr, data_len)
|
||||||
|
.map_err(ExecuteError::GetHostAddress)?;
|
||||||
|
assert!(origin_ptr.len() >= data_len);
|
||||||
|
let origin_ptr = origin_ptr.ptr_guard();
|
||||||
|
|
||||||
|
// Verify the buffer alignment.
|
||||||
|
// In case it's not properly aligned, an intermediate buffer is
|
||||||
|
// created with the correct alignment, and a copy from/to the
|
||||||
|
// origin buffer is performed, depending on the type of operation.
|
||||||
|
let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(SECTOR_SIZE) {
|
||||||
|
origin_ptr.as_ptr() as *mut libc::c_void
|
||||||
|
} else {
|
||||||
|
let layout = Layout::from_size_align(data_len, SECTOR_SIZE as usize).unwrap();
|
||||||
|
// SAFETY: layout has non-zero size
|
||||||
|
let aligned_ptr = unsafe { alloc_zeroed(layout) };
|
||||||
|
if aligned_ptr.is_null() {
|
||||||
|
return Err(ExecuteError::TemporaryBufferAllocation(
|
||||||
|
io::Error::last_os_error(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to perform the copy beforehand in case we're writing
|
||||||
|
// data out.
|
||||||
|
if request_type == RequestType::Out {
|
||||||
|
// SAFETY: destination buffer has been allocated with
|
||||||
|
// the proper size.
|
||||||
|
unsafe { std::ptr::copy(origin_ptr.as_ptr(), aligned_ptr, data_len) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store both origin and aligned pointers for complete_async()
|
||||||
|
// to process them.
|
||||||
|
self.aligned_operations.push(AlignedOperation {
|
||||||
|
origin_ptr: origin_ptr.as_ptr() as u64,
|
||||||
|
aligned_ptr: aligned_ptr as u64,
|
||||||
|
size: data_len,
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
aligned_ptr as *mut libc::c_void
|
||||||
|
};
|
||||||
|
|
||||||
|
let iovec = libc::iovec {
|
||||||
|
iov_base,
|
||||||
|
iov_len: data_len as libc::size_t,
|
||||||
|
};
|
||||||
|
iovecs.push(iovec);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = ExecuteAsync {
|
||||||
|
async_complete: true,
|
||||||
|
batch_request: None,
|
||||||
|
};
|
||||||
|
// Queue operations expected to be submitted.
|
||||||
|
match request_type {
|
||||||
|
RequestType::In => {
|
||||||
|
for (data_addr, data_len) in &self.data_descriptors {
|
||||||
|
mem.get_slice(*data_addr, *data_len as usize)
|
||||||
|
.map_err(ExecuteError::GetHostAddress)?
|
||||||
|
.bitmap()
|
||||||
|
.mark_dirty(0, *data_len as usize);
|
||||||
|
}
|
||||||
|
if disk_image.batch_requests_enabled() {
|
||||||
|
ret.batch_request = Some(BatchRequest {
|
||||||
|
offset,
|
||||||
|
iovecs,
|
||||||
|
user_data,
|
||||||
|
request_type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
disk_image
|
||||||
|
.read_vectored(offset, &iovecs, user_data)
|
||||||
|
.map_err(ExecuteError::AsyncRead)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequestType::Out => {
|
||||||
|
if disk_image.batch_requests_enabled() {
|
||||||
|
ret.batch_request = Some(BatchRequest {
|
||||||
|
offset,
|
||||||
|
iovecs,
|
||||||
|
user_data,
|
||||||
|
request_type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
disk_image
|
||||||
|
.write_vectored(offset, &iovecs, user_data)
|
||||||
|
.map_err(ExecuteError::AsyncWrite)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequestType::Flush => {
|
||||||
|
disk_image
|
||||||
|
.fsync(Some(user_data))
|
||||||
|
.map_err(ExecuteError::AsyncFlush)?;
|
||||||
|
}
|
||||||
|
RequestType::GetDeviceId => {
|
||||||
|
let (data_addr, data_len) = if self.data_descriptors.len() == 1 {
|
||||||
|
(self.data_descriptors[0].0, self.data_descriptors[0].1)
|
||||||
|
} else {
|
||||||
|
return Err(ExecuteError::BadRequest(Error::TooManyDescriptors));
|
||||||
|
};
|
||||||
|
if (data_len as usize) < serial.len() {
|
||||||
|
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
||||||
|
}
|
||||||
|
mem.write_slice(serial, data_addr)
|
||||||
|
.map_err(ExecuteError::Write)?;
|
||||||
|
ret.async_complete = false;
|
||||||
|
return Ok(ret);
|
||||||
|
}
|
||||||
|
RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete_async(&mut self) -> result::Result<(), Error> {
|
||||||
|
for aligned_operation in self.aligned_operations.drain(..) {
|
||||||
|
// We need to perform the copy after the data has been read inside
|
||||||
|
// the aligned buffer in case we're reading data in.
|
||||||
|
if self.request_type == RequestType::In {
|
||||||
|
// SAFETY: origin buffer has been allocated with the
|
||||||
|
// proper size.
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy(
|
||||||
|
aligned_operation.aligned_ptr as *const u8,
|
||||||
|
aligned_operation.origin_ptr as *mut u8,
|
||||||
|
aligned_operation.size,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the temporary aligned buffer.
|
||||||
|
// SAFETY: aligned_ptr was allocated by alloc_zeroed with the same
|
||||||
|
// layout
|
||||||
|
unsafe {
|
||||||
|
dealloc(
|
||||||
|
aligned_operation.aligned_ptr as *mut u8,
|
||||||
|
aligned_operation.layout,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_writeback(&mut self, writeback: bool) {
|
||||||
|
self.writeback = writeback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
#[repr(C, packed)]
|
#[repr(C, packed)]
|
||||||
pub struct VirtioBlockConfig {
|
pub struct VirtioBlockConfig {
|
||||||
@@ -332,126 +685,6 @@ pub fn block_io_uring_is_supported() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe whether the file/device supports punch hole and zero range
|
|
||||||
pub fn probe_sparse_support(file: &File) -> bool {
|
|
||||||
let fd = file.as_raw_fd();
|
|
||||||
|
|
||||||
let is_block_device = {
|
|
||||||
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
|
|
||||||
// SAFETY: FFI call with valid fd and buffer
|
|
||||||
let ret = unsafe { libc::fstat(fd, stat.as_mut_ptr()) };
|
|
||||||
if ret != 0 {
|
|
||||||
warn!(
|
|
||||||
"Failed to stat file descriptor for sparse probe: {}",
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// SAFETY: stat result is valid at this point
|
|
||||||
unsafe { (*stat.as_ptr()).st_mode & S_IFMT == S_IFBLK }
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_block_device {
|
|
||||||
probe_block_device_sparse_support(fd)
|
|
||||||
} else {
|
|
||||||
probe_file_sparse_support(fd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Probe sparse support for a regular file using fallocate().
|
|
||||||
fn probe_file_sparse_support(fd: libc::c_int) -> bool {
|
|
||||||
// SAFETY: FFI call with valid fd
|
|
||||||
let file_size = unsafe { libc::lseek(fd, 0, libc::SEEK_END) };
|
|
||||||
if file_size < 0 {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
warn!("Failed to get file size for sparse probe: {err}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid fd, probing past EOF is safe with KEEP_SIZE
|
|
||||||
let punch_hole =
|
|
||||||
unsafe { libc::fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, file_size, 1) }
|
|
||||||
== 0;
|
|
||||||
|
|
||||||
if !punch_hole {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.raw_os_error() == Some(libc::EOPNOTSUPP) {
|
|
||||||
debug!("File does not support FALLOC_FL_PUNCH_HOLE: {err}");
|
|
||||||
} else {
|
|
||||||
debug!("PUNCH_HOLE probe returned unexpected error: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid fd, probing past EOF is safe with KEEP_SIZE
|
|
||||||
let zero_range =
|
|
||||||
unsafe { libc::fallocate(fd, FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, file_size, 1) }
|
|
||||||
== 0;
|
|
||||||
|
|
||||||
if !zero_range {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.raw_os_error() == Some(libc::EOPNOTSUPP) {
|
|
||||||
debug!("File does not support FALLOC_FL_ZERO_RANGE: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let supported = punch_hole || zero_range;
|
|
||||||
info!(
|
|
||||||
"Probed file sparse support: punch_hole={punch_hole}, zero_range={zero_range} => {supported}"
|
|
||||||
);
|
|
||||||
supported
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Probe sparse support for a block device.
|
|
||||||
///
|
|
||||||
/// Block devices always report sparse support. `BLKZEROOUT` is guaranteed to
|
|
||||||
/// succeed as the kernel provides a software fallback writing explicit zeros
|
|
||||||
/// when the hardware lacks a native write zeroes command. `BLKDISCARD` may fail
|
|
||||||
/// at runtime with `EOPNOTSUPP` on devices without trim or discard support, but
|
|
||||||
/// Linux guests handle this gracefully by ceasing discard requests.
|
|
||||||
///
|
|
||||||
/// There is no non destructive read only ioctl to query block device discard
|
|
||||||
/// or write zeroes capabilities.
|
|
||||||
fn probe_block_device_sparse_support(_fd: libc::c_int) -> bool {
|
|
||||||
info!("Block device: assuming sparse support");
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preallocate disk space for a disk image file.
|
|
||||||
///
|
|
||||||
/// Uses `fallocate()` to allocate all disk space upfront, ensuring storage
|
|
||||||
/// availability and reducing fragmentation. Allocating all blocks upfront is
|
|
||||||
/// more likely to place them contiguously than allocating on demand during
|
|
||||||
/// random writes.
|
|
||||||
pub fn preallocate_disk<P: AsRef<Path>>(file: &File, path: P) {
|
|
||||||
let size = match file.metadata() {
|
|
||||||
Ok(m) => m.len(),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to get metadata for {:?}: {}", path.as_ref(), e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if size == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid file descriptor and size
|
|
||||||
let ret = unsafe { libc::fallocate(file.as_raw_fd(), 0, 0, size as libc::off_t) };
|
|
||||||
|
|
||||||
if ret != 0 {
|
|
||||||
warn!(
|
|
||||||
"Failed to preallocate disk space for {:?}: {}",
|
|
||||||
path.as_ref(),
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"Preallocated {size} bytes for disk image {:?}",
|
|
||||||
path.as_ref()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AsyncAdaptor {
|
pub trait AsyncAdaptor {
|
||||||
fn read_vectored_sync(
|
fn read_vectored_sync(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -616,27 +849,14 @@ pub fn read_aligned_block_size(f: &mut File) -> std::io::Result<Vec<u8>> {
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a disk image file, returning a [`BlockError`] with path context
|
|
||||||
/// on failure.
|
|
||||||
pub fn open_disk_image(path: &Path, options: &OpenOptions) -> BlockResult<File> {
|
|
||||||
options.open(path).map_err(|e| {
|
|
||||||
BlockError::new(BlockErrorKind::Io, e)
|
|
||||||
.with_op(ErrorOp::Open)
|
|
||||||
.with_path(path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine image type through file parsing.
|
/// Determine image type through file parsing.
|
||||||
pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
|
pub fn detect_image_type(f: &mut File) -> std::io::Result<ImageType> {
|
||||||
let block = read_aligned_block_size(f)
|
let block = read_aligned_block_size(f)?;
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?;
|
|
||||||
|
|
||||||
// Check 4 first bytes to get the header value and determine the image type
|
// Check 4 first bytes to get the header value and determine the image type
|
||||||
let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC {
|
let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC {
|
||||||
ImageType::Qcow2
|
ImageType::Qcow2
|
||||||
} else if vhd::is_fixed_vhd(f)
|
} else if vhd::is_fixed_vhd(f)? {
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?
|
|
||||||
{
|
|
||||||
ImageType::FixedVhd
|
ImageType::FixedVhd
|
||||||
} else if u64::from_le_bytes(block[0..8].try_into().unwrap()) == VHDX_SIGN {
|
} else if u64::from_le_bytes(block[0..8].try_into().unwrap()) == VHDX_SIGN {
|
||||||
ImageType::Vhdx
|
ImageType::Vhdx
|
||||||
@@ -681,36 +901,6 @@ ioctl_io_nr!(BLKSSZGET, 0x12, 104);
|
|||||||
ioctl_io_nr!(BLKPBSZGET, 0x12, 123);
|
ioctl_io_nr!(BLKPBSZGET, 0x12, 123);
|
||||||
ioctl_io_nr!(BLKIOMIN, 0x12, 120);
|
ioctl_io_nr!(BLKIOMIN, 0x12, 120);
|
||||||
ioctl_io_nr!(BLKIOOPT, 0x12, 121);
|
ioctl_io_nr!(BLKIOOPT, 0x12, 121);
|
||||||
ioctl_ior_nr!(BLKGETSIZE64, 0x12, 114, u64);
|
|
||||||
|
|
||||||
/// Returns `(logical_size, physical_size)` in bytes for regular files and block devices.
|
|
||||||
///
|
|
||||||
/// For regular files, logical size is `st_size` and physical size is
|
|
||||||
/// `st_blocks * 512` (actual host allocation). For block devices both
|
|
||||||
/// values equal the `BLKGETSIZE64` result.
|
|
||||||
pub fn query_device_size(file: &File) -> io::Result<(u64, u64)> {
|
|
||||||
let m = file.metadata()?;
|
|
||||||
if m.is_file() {
|
|
||||||
// st_blocks is always in 512-byte units on Linux
|
|
||||||
Ok((m.len(), m.st_blocks() * 512))
|
|
||||||
} else if m.file_type().is_block_device() {
|
|
||||||
let mut size: u64 = 0;
|
|
||||||
// SAFETY: BLKGETSIZE64 reads the device size into a u64 pointer.
|
|
||||||
let ret = unsafe { libc::ioctl(file.as_raw_fd(), BLKGETSIZE64() as _, &mut size) };
|
|
||||||
if ret != 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok((size, size))
|
|
||||||
} else {
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!(
|
|
||||||
"disk image must be a regular file or block device, is: {:?}",
|
|
||||||
m.file_type()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
enum BlockSize {
|
enum BlockSize {
|
||||||
@@ -757,73 +947,8 @@ impl DiskTopology {
|
|||||||
Ok(block_size)
|
Ok(block_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query the O_DIRECT alignment requirement for a regular file.
|
|
||||||
///
|
|
||||||
/// Uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain the exact
|
|
||||||
/// memory and offset alignment the kernel requires for direct I/O on
|
|
||||||
/// this specific file. Unlike `fstatvfs().f_bsize`, which only returns
|
|
||||||
/// the filesystem's preferred I/O block size, `STATX_DIOALIGN` reports
|
|
||||||
/// the true per-file DIO constraints accounting for the filesystem,
|
|
||||||
/// underlying block device, and any stacking (loop, dm, etc.).
|
|
||||||
fn query_file_alignment(f: &File) -> u64 {
|
|
||||||
// The libc crate does not expose statx / STATX_DIOALIGN on all
|
|
||||||
// targets (e.g. musl), so define the constant and a minimal repr(C)
|
|
||||||
// struct locally and invoke the syscall directly.
|
|
||||||
const STATX_DIOALIGN: u32 = 0x2000;
|
|
||||||
|
|
||||||
// Minimal statx layout, only the needed fields,
|
|
||||||
// everything else is padding.
|
|
||||||
#[repr(C)]
|
|
||||||
struct Statx {
|
|
||||||
stx_mask: u32,
|
|
||||||
_pad: [u8; 148],
|
|
||||||
stx_dio_mem_align: u32,
|
|
||||||
stx_dio_offset_align: u32,
|
|
||||||
_pad2: [u8; 96],
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stx = mem::MaybeUninit::<Statx>::zeroed();
|
|
||||||
// SAFETY: FFI syscall with valid fd and correctly sized buffer.
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::syscall(
|
|
||||||
libc::SYS_statx,
|
|
||||||
f.as_raw_fd(),
|
|
||||||
c"".as_ptr(),
|
|
||||||
libc::AT_EMPTY_PATH,
|
|
||||||
STATX_DIOALIGN,
|
|
||||||
stx.as_mut_ptr(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ret == 0 {
|
|
||||||
// SAFETY: statx succeeded, the struct is fully initialized.
|
|
||||||
let stx = unsafe { stx.assume_init() };
|
|
||||||
if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 {
|
|
||||||
let align = cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64;
|
|
||||||
debug!("statx(STATX_DIOALIGN) returned alignment {align}");
|
|
||||||
return align;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}");
|
|
||||||
SECTOR_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn probe(f: &File) -> std::io::Result<Self> {
|
pub fn probe(f: &File) -> std::io::Result<Self> {
|
||||||
if !Self::is_block_device(f)? {
|
if !Self::is_block_device(f)? {
|
||||||
// For regular files opened with O_DIRECT, the logical block size
|
|
||||||
// must reflect the filesystem DIO alignment so the guest issues
|
|
||||||
// correctly sized I/O.
|
|
||||||
// SAFETY: fcntl(F_GETFL) is always safe on a valid fd.
|
|
||||||
let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) };
|
|
||||||
if flags >= 0 && (flags & libc::O_DIRECT) != 0 {
|
|
||||||
let alignment = Self::query_file_alignment(f);
|
|
||||||
return Ok(DiskTopology {
|
|
||||||
logical_block_size: alignment,
|
|
||||||
physical_block_size: alignment,
|
|
||||||
minimum_io_size: alignment,
|
|
||||||
optimal_io_size: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Ok(DiskTopology::default());
|
return Ok(DiskTopology::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,192 +960,3 @@ impl DiskTopology {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::{ptr, slice};
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_probe_regular_file_returns_valid_alignment() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut f = temp_file.into_file();
|
|
||||||
f.write_all(&[0u8; 4096]).unwrap();
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
|
|
||||||
let topo = DiskTopology::probe(&f).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
topo.logical_block_size, SECTOR_SIZE,
|
|
||||||
"probe() should return {SECTOR_SIZE} for regular files without O_DIRECT, got {}",
|
|
||||||
topo.logical_block_size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_probe_regular_file_with_direct_returns_dio_alignment() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let path = temp_file.as_path().to_owned();
|
|
||||||
{
|
|
||||||
let f = temp_file.as_file();
|
|
||||||
f.set_len(1 << 20).unwrap(); // 1 MiB
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let f = OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.custom_flags(libc::O_DIRECT)
|
|
||||||
.open(&path)
|
|
||||||
.unwrap();
|
|
||||||
let topo = DiskTopology::probe(&f).unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
topo.logical_block_size.is_power_of_two(),
|
|
||||||
"logical_block_size {} is not a power of two",
|
|
||||||
topo.logical_block_size
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
topo.logical_block_size >= SECTOR_SIZE,
|
|
||||||
"logical_block_size {} is less than SECTOR_SIZE ({SECTOR_SIZE})",
|
|
||||||
topo.logical_block_size
|
|
||||||
);
|
|
||||||
|
|
||||||
let alignment = topo.logical_block_size as usize;
|
|
||||||
let layout = Layout::from_size_align(4096, alignment);
|
|
||||||
assert!(
|
|
||||||
layout.is_ok(),
|
|
||||||
"Layout::from_size_align(4096, {alignment}) failed: {:?}",
|
|
||||||
layout.err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dio_write_read_with_probed_alignment() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let path = temp_file.as_path().to_owned();
|
|
||||||
{
|
|
||||||
let f = temp_file.as_file();
|
|
||||||
f.set_len(1 << 20).unwrap(); // 1 MiB
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let f = OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.custom_flags(libc::O_DIRECT)
|
|
||||||
.open(&path)
|
|
||||||
.unwrap();
|
|
||||||
let topo = DiskTopology::probe(&f).unwrap();
|
|
||||||
let alignment = topo.logical_block_size as usize;
|
|
||||||
|
|
||||||
let layout = Layout::from_size_align(alignment, alignment).unwrap();
|
|
||||||
// SAFETY: layout is valid (non-zero, power-of-two alignment).
|
|
||||||
let buf = unsafe { alloc_zeroed(layout) };
|
|
||||||
assert!(!buf.is_null());
|
|
||||||
|
|
||||||
// SAFETY: buf is valid for `alignment` bytes.
|
|
||||||
unsafe { ptr::write_bytes(buf, 0xAB, alignment) };
|
|
||||||
|
|
||||||
// SAFETY: buf is aligned and sized for O_DIRECT; fd is valid.
|
|
||||||
let written = unsafe { libc::pwrite(f.as_raw_fd(), buf.cast(), alignment, 0) };
|
|
||||||
assert_eq!(
|
|
||||||
written as usize,
|
|
||||||
alignment,
|
|
||||||
"O_DIRECT pwrite failed: {}",
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
|
|
||||||
// SAFETY: buf is valid for `alignment` bytes.
|
|
||||||
unsafe { ptr::write_bytes(buf, 0x00, alignment) };
|
|
||||||
// SAFETY: buf is aligned and sized for O_DIRECT; fd is valid.
|
|
||||||
let read = unsafe { libc::pread(f.as_raw_fd(), buf.cast(), alignment, 0) };
|
|
||||||
assert_eq!(
|
|
||||||
read as usize,
|
|
||||||
alignment,
|
|
||||||
"O_DIRECT pread failed: {}",
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
|
|
||||||
// SAFETY: buf is valid for `alignment` bytes after successful pread.
|
|
||||||
let slice = unsafe { slice::from_raw_parts(buf, alignment) };
|
|
||||||
assert!(
|
|
||||||
slice.iter().all(|&b| b == 0xAB),
|
|
||||||
"Data mismatch after O_DIRECT roundtrip"
|
|
||||||
);
|
|
||||||
|
|
||||||
// SAFETY: buf was allocated with this layout via alloc_zeroed.
|
|
||||||
unsafe { dealloc(buf, layout) };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_device_size_regular_file() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut f = temp_file.into_file();
|
|
||||||
// 5 sectors + 13 extra bytes - not page aligned, not sectoraligned
|
|
||||||
f.write_all(&[0xAB; 5 * 512 + 13]).unwrap();
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
|
|
||||||
let (logical, physical) = query_device_size(&f).unwrap();
|
|
||||||
assert_eq!(logical, 5 * 512 + 13);
|
|
||||||
assert!(physical > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_device_size_sparse_file_punch_hole() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let f = temp_file.as_file();
|
|
||||||
// Allocate 1 MiB
|
|
||||||
let size: i64 = 1 << 20;
|
|
||||||
f.set_len(size as u64).unwrap();
|
|
||||||
// SAFETY: fd is valid, range is within file size.
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
f.as_raw_fd(),
|
|
||||||
0, // allocate
|
|
||||||
0,
|
|
||||||
size,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
assert_eq!(ret, 0, "fallocate failed: {}", io::Error::last_os_error());
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
|
|
||||||
let (log_before, phys_before) = query_device_size(f).unwrap();
|
|
||||||
assert_eq!(log_before, size as u64);
|
|
||||||
assert_eq!(phys_before, size as u64);
|
|
||||||
|
|
||||||
// Punch a hole in the middle 512 KiB
|
|
||||||
// SAFETY: fd is valid, range is within file size.
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
f.as_raw_fd(),
|
|
||||||
libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
|
|
||||||
size / 4,
|
|
||||||
size / 2,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
assert_eq!(ret, 0, "punch hole failed: {}", io::Error::last_os_error());
|
|
||||||
f.sync_all().unwrap();
|
|
||||||
|
|
||||||
let (logical, physical) = query_device_size(f).unwrap();
|
|
||||||
assert_eq!(logical, size as u64, "logical size must not change");
|
|
||||||
assert!(
|
|
||||||
physical < logical,
|
|
||||||
"physical ({physical}) should be less than logical ({logical}) after punch hole"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_device_size_rejects_char_device() {
|
|
||||||
let f = std::fs::File::open("/dev/zero").unwrap();
|
|
||||||
let err = query_device_size(&f).unwrap_err();
|
|
||||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
// Copyright © 2021 Intel Corporation
|
|
||||||
//
|
|
||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
//! Thread safe backing file readers for QCOW2 images.
|
|
||||||
|
|
||||||
use std::io;
|
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
|
||||||
use crate::qcow::decoder::Decoder;
|
|
||||||
use crate::qcow::metadata::{BackingRead, ClusterReadMapping, QcowMetadata};
|
|
||||||
use crate::qcow::{BackingFile, BackingKind, Error as QcowError};
|
|
||||||
use crate::qcow_common::{decompress_cluster, pread_alloc, pread_exact};
|
|
||||||
|
|
||||||
/// Raw backing file using pread64 on a duplicated fd.
|
|
||||||
pub(crate) struct RawBacking {
|
|
||||||
pub(crate) fd: OwnedFd,
|
|
||||||
pub(crate) virtual_size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: The only I/O operation is pread64 which is position independent
|
|
||||||
// and safe for concurrent use from multiple threads.
|
|
||||||
unsafe impl Sync for RawBacking {}
|
|
||||||
|
|
||||||
impl BackingRead for RawBacking {
|
|
||||||
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
||||||
if address >= self.virtual_size {
|
|
||||||
buf.fill(0);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let available = (self.virtual_size - address) as usize;
|
|
||||||
if available >= buf.len() {
|
|
||||||
pread_exact(self.fd.as_raw_fd(), buf, address)
|
|
||||||
} else {
|
|
||||||
pread_exact(self.fd.as_raw_fd(), &mut buf[..available], address)?;
|
|
||||||
buf[available..].fill(0);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// QCOW2 image used as a backing file for another QCOW2 image.
|
|
||||||
///
|
|
||||||
/// Resolves guest offsets through the QCOW2 cluster mapping (L1/L2
|
|
||||||
/// tables, refcounts) before reading the underlying data. Read only
|
|
||||||
/// because backing files never receive writes. Nested backing chains
|
|
||||||
/// are handled recursively via the optional `backing_file` field.
|
|
||||||
pub(crate) struct Qcow2Backing {
|
|
||||||
pub(crate) metadata: Arc<QcowMetadata>,
|
|
||||||
pub(crate) data_fd: OwnedFd,
|
|
||||||
pub(crate) backing_file: Option<Arc<dyn BackingRead>>,
|
|
||||||
pub(crate) cluster_size: u64,
|
|
||||||
pub(crate) decoder: Arc<dyn Decoder>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: All reads go through QcowMetadata which uses RwLock
|
|
||||||
// and pread64 which is position independent and thread safe.
|
|
||||||
unsafe impl Sync for Qcow2Backing {}
|
|
||||||
|
|
||||||
impl BackingRead for Qcow2Backing {
|
|
||||||
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
||||||
let virtual_size = self.metadata.virtual_size();
|
|
||||||
if address >= virtual_size {
|
|
||||||
buf.fill(0);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let available = (virtual_size - address) as usize;
|
|
||||||
if available < buf.len() {
|
|
||||||
self.read_clusters(address, &mut buf[..available])?;
|
|
||||||
buf[available..].fill(0);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.read_clusters(address, buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Qcow2Backing {
|
|
||||||
/// Resolve cluster mappings via metadata then read allocated clusters
|
|
||||||
/// with pread64.
|
|
||||||
fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
||||||
let total_len = buf.len();
|
|
||||||
let has_backing = self.backing_file.is_some();
|
|
||||||
|
|
||||||
let mappings = self
|
|
||||||
.metadata
|
|
||||||
.map_clusters_for_read(address, total_len, has_backing)?;
|
|
||||||
|
|
||||||
let mut buf_offset = 0usize;
|
|
||||||
for mapping in mappings {
|
|
||||||
match mapping {
|
|
||||||
ClusterReadMapping::Zero { length } => {
|
|
||||||
buf[buf_offset..buf_offset + length as usize].fill(0);
|
|
||||||
buf_offset += length as usize;
|
|
||||||
}
|
|
||||||
ClusterReadMapping::Allocated {
|
|
||||||
offset: host_offset,
|
|
||||||
length,
|
|
||||||
} => {
|
|
||||||
pread_exact(
|
|
||||||
self.data_fd.as_raw_fd(),
|
|
||||||
&mut buf[buf_offset..buf_offset + length as usize],
|
|
||||||
host_offset,
|
|
||||||
)?;
|
|
||||||
buf_offset += length as usize;
|
|
||||||
}
|
|
||||||
ClusterReadMapping::Compressed {
|
|
||||||
host_offset,
|
|
||||||
compressed_size,
|
|
||||||
cluster_offset,
|
|
||||||
length,
|
|
||||||
} => {
|
|
||||||
let compressed =
|
|
||||||
pread_alloc(self.data_fd.as_raw_fd(), host_offset, compressed_size)?;
|
|
||||||
let decompressed = decompress_cluster(
|
|
||||||
&compressed,
|
|
||||||
self.cluster_size as usize,
|
|
||||||
&*self.decoder,
|
|
||||||
)?;
|
|
||||||
buf[buf_offset..buf_offset + length]
|
|
||||||
.copy_from_slice(&decompressed[cluster_offset..cluster_offset + length]);
|
|
||||||
buf_offset += length;
|
|
||||||
}
|
|
||||||
ClusterReadMapping::Backing {
|
|
||||||
offset: backing_offset,
|
|
||||||
length,
|
|
||||||
} => {
|
|
||||||
self.backing_file.as_ref().unwrap().read_at(
|
|
||||||
backing_offset,
|
|
||||||
&mut buf[buf_offset..buf_offset + length as usize],
|
|
||||||
)?;
|
|
||||||
buf_offset += length as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Qcow2Backing {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.metadata.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a thread safe backing file reader.
|
|
||||||
pub fn shared_backing_from(bf: BackingFile) -> BlockResult<Arc<dyn BackingRead>> {
|
|
||||||
let (kind, virtual_size) = bf.into_kind();
|
|
||||||
|
|
||||||
let dup_fd = |fd: BorrowedFd<'_>| -> BlockResult<OwnedFd> {
|
|
||||||
fd.try_clone_to_owned().map_err(|e| {
|
|
||||||
BlockError::new(
|
|
||||||
BlockErrorKind::Io,
|
|
||||||
QcowError::BackingFileIo(String::new(), e),
|
|
||||||
)
|
|
||||||
.with_op(ErrorOp::DupBackingFd)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
match kind {
|
|
||||||
BackingKind::Raw(raw_file) => {
|
|
||||||
let fd = dup_fd(raw_file.as_fd())?;
|
|
||||||
Ok(Arc::new(RawBacking { fd, virtual_size }))
|
|
||||||
}
|
|
||||||
BackingKind::Qcow { inner, backing } => {
|
|
||||||
let data_fd = dup_fd(inner.raw_file.as_fd())?;
|
|
||||||
let metadata = Arc::new(QcowMetadata::new(*inner));
|
|
||||||
Ok(Arc::new(Qcow2Backing {
|
|
||||||
cluster_size: metadata.cluster_size(),
|
|
||||||
decoder: metadata.decoder(),
|
|
||||||
metadata,
|
|
||||||
data_fd,
|
|
||||||
backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
BackingKind::QcowFile(_) => {
|
|
||||||
unreachable!("QcowFile variant is only used by set_backing_file() in tests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ pub enum Error {
|
|||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
/// Generic trait for decoding zlib/zstd formats
|
/// Generic trait for decoding zlib/zstd formats
|
||||||
pub trait Decoder: Send + Sync {
|
pub trait Decoder {
|
||||||
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize>;
|
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,605 +0,0 @@
|
|||||||
// Copyright 2018 The Chromium OS Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE-BSD-3-Clause file.
|
|
||||||
//
|
|
||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
//! QCOW2 header parsing, validation, and creation.
|
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
|
||||||
use std::mem::size_of;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use bitflags::bitflags;
|
|
||||||
use vmm_sys_util::file_traits::FileSync;
|
|
||||||
|
|
||||||
use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder};
|
|
||||||
use super::qcow_raw_file::BeUint;
|
|
||||||
use super::raw_file::RawFile;
|
|
||||||
use super::{Error, Result, div_round_up_u32, div_round_up_u64};
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum ImageType {
|
|
||||||
Raw,
|
|
||||||
Qcow2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ImageType {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
match self {
|
|
||||||
ImageType::Raw => write!(f, "raw"),
|
|
||||||
ImageType::Qcow2 => write!(f, "qcow2"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ImageType {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"raw" => Ok(ImageType::Raw),
|
|
||||||
"qcow2" => Ok(ImageType::Qcow2),
|
|
||||||
_ => Err(Error::UnsupportedBackingFileFormat(s.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum CompressionType {
|
|
||||||
Zlib,
|
|
||||||
Zstd,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BackingFileConfig {
|
|
||||||
pub path: String,
|
|
||||||
// If this is None, we will autodetect it.
|
|
||||||
pub format: Option<ImageType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maximum data size supported.
|
|
||||||
pub(super) const MAX_QCOW_FILE_SIZE: u64 = 0x01 << 44; // 16 TB.
|
|
||||||
|
|
||||||
// QCOW magic constant that starts the header.
|
|
||||||
pub(super) const QCOW_MAGIC: u32 = 0x5146_49fb;
|
|
||||||
// Default to a cluster size of 2^DEFAULT_CLUSTER_BITS
|
|
||||||
pub(super) const DEFAULT_CLUSTER_BITS: u32 = 16;
|
|
||||||
// Limit clusters to reasonable sizes. Choose the same limits as qemu. Making the clusters smaller
|
|
||||||
// increases the amount of overhead for book keeping.
|
|
||||||
pub(super) const MIN_CLUSTER_BITS: u32 = 9;
|
|
||||||
pub(super) const MAX_CLUSTER_BITS: u32 = 21;
|
|
||||||
// The L1 and RefCount table are kept in RAM, only handle files that require less than 35M entries.
|
|
||||||
// This easily covers 1 TB files. When support for bigger files is needed the assumptions made to
|
|
||||||
// keep these tables in RAM needs to be thrown out.
|
|
||||||
pub(super) const MAX_RAM_POINTER_TABLE_SIZE: u64 = 35_000_000;
|
|
||||||
// 16-bit refcounts.
|
|
||||||
pub(super) const DEFAULT_REFCOUNT_ORDER: u32 = 4;
|
|
||||||
|
|
||||||
pub(super) const V2_BARE_HEADER_SIZE: u32 = 72;
|
|
||||||
pub(super) const V3_BARE_HEADER_SIZE: u32 = 104;
|
|
||||||
pub(super) const AUTOCLEAR_FEATURES_OFFSET: u64 = 88;
|
|
||||||
|
|
||||||
pub(super) const COMPATIBLE_FEATURES_LAZY_REFCOUNTS: u64 = 1;
|
|
||||||
|
|
||||||
// Compression types as defined in https://www.qemu.org/docs/master/interop/qcow2.html
|
|
||||||
const COMPRESSION_TYPE_ZLIB: u64 = 0; // zlib/deflate <https://www.ietf.org/rfc/rfc1951.txt>
|
|
||||||
const COMPRESSION_TYPE_ZSTD: u64 = 1; // zstd <http://github.com/facebook/zstd>
|
|
||||||
|
|
||||||
// Header extension types
|
|
||||||
pub(super) const HEADER_EXT_END: u32 = 0x00000000;
|
|
||||||
// Backing file format name (raw, qcow2)
|
|
||||||
pub(super) const HEADER_EXT_BACKING_FORMAT: u32 = 0xe2792aca;
|
|
||||||
// Feature name table
|
|
||||||
const HEADER_EXT_FEATURE_NAME_TABLE: u32 = 0x6803f857;
|
|
||||||
|
|
||||||
// Feature name table entry type incompatible
|
|
||||||
const FEAT_TYPE_INCOMPATIBLE: u8 = 0;
|
|
||||||
|
|
||||||
bitflags! {
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct IncompatFeatures: u64 {
|
|
||||||
const DIRTY = 1 << 0;
|
|
||||||
const CORRUPT = 1 << 1;
|
|
||||||
const DATA_FILE = 1 << 2;
|
|
||||||
const COMPRESSION = 1 << 3;
|
|
||||||
const EXTENDED_L2 = 1 << 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IncompatFeatures {
|
|
||||||
/// Features supported by this implementation.
|
|
||||||
pub(super) const SUPPORTED: IncompatFeatures = IncompatFeatures::DIRTY
|
|
||||||
.union(IncompatFeatures::CORRUPT)
|
|
||||||
.union(IncompatFeatures::COMPRESSION);
|
|
||||||
|
|
||||||
/// Get the fallback name for a known feature bit.
|
|
||||||
fn flag_name(bit: u8) -> Option<&'static str> {
|
|
||||||
Some(match Self::from_bits_truncate(1u64 << bit) {
|
|
||||||
Self::DIRTY => "dirty bit",
|
|
||||||
Self::CORRUPT => "corrupt bit",
|
|
||||||
Self::DATA_FILE => "external data file",
|
|
||||||
Self::EXTENDED_L2 => "extended L2 entries",
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type for unsupported incompatible features.
|
|
||||||
#[derive(Debug, Clone, thiserror::Error)]
|
|
||||||
pub struct MissingFeatureError {
|
|
||||||
/// Unsupported feature bits.
|
|
||||||
features: IncompatFeatures,
|
|
||||||
/// Feature name table from the qcow2 image.
|
|
||||||
feature_names: Vec<(u8, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MissingFeatureError {
|
|
||||||
pub(super) fn new(features: IncompatFeatures, feature_names: Vec<(u8, String)>) -> Self {
|
|
||||||
Self {
|
|
||||||
features,
|
|
||||||
feature_names,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for MissingFeatureError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
let names: Vec<String> = (0u8..64)
|
|
||||||
.filter(|&bit| self.features.bits() & (1u64 << bit) != 0)
|
|
||||||
.map(|bit| {
|
|
||||||
// First try the image's feature name table
|
|
||||||
self.feature_names
|
|
||||||
.iter()
|
|
||||||
.find(|(b, _)| *b == bit)
|
|
||||||
.map(|(_, name)| name.clone())
|
|
||||||
// Then try hardcoded fallback names
|
|
||||||
.or_else(|| IncompatFeatures::flag_name(bit).map(|s| s.to_string()))
|
|
||||||
// Finally, use generic description
|
|
||||||
.unwrap_or_else(|| format!("unknown feature bit {bit}"))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
write!(f, "Missing features: {}", names.join(", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The format supports a "header extension area", that crosvm does not use.
|
|
||||||
const QCOW_EMPTY_HEADER_EXTENSION_SIZE: u32 = 8;
|
|
||||||
|
|
||||||
// Defined by the specification
|
|
||||||
const MAX_BACKING_FILE_SIZE: u32 = 1023;
|
|
||||||
|
|
||||||
/// Contains the information from the header of a qcow file.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct QcowHeader {
|
|
||||||
pub magic: u32,
|
|
||||||
pub version: u32,
|
|
||||||
|
|
||||||
pub backing_file_offset: u64,
|
|
||||||
pub backing_file_size: u32,
|
|
||||||
|
|
||||||
pub cluster_bits: u32,
|
|
||||||
pub size: u64,
|
|
||||||
pub crypt_method: u32,
|
|
||||||
|
|
||||||
pub l1_size: u32,
|
|
||||||
pub l1_table_offset: u64,
|
|
||||||
|
|
||||||
pub refcount_table_offset: u64,
|
|
||||||
pub refcount_table_clusters: u32,
|
|
||||||
|
|
||||||
pub nb_snapshots: u32,
|
|
||||||
pub snapshots_offset: u64,
|
|
||||||
|
|
||||||
// v3 entries
|
|
||||||
pub incompatible_features: u64,
|
|
||||||
pub compatible_features: u64,
|
|
||||||
pub autoclear_features: u64,
|
|
||||||
pub refcount_order: u32,
|
|
||||||
pub header_size: u32,
|
|
||||||
pub compression_type: CompressionType,
|
|
||||||
|
|
||||||
// Post-header entries
|
|
||||||
pub backing_file: Option<BackingFileConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QcowHeader {
|
|
||||||
/// Read header extensions, optionally collecting feature names for error reporting.
|
|
||||||
pub(super) fn read_header_extensions(
|
|
||||||
f: &mut RawFile,
|
|
||||||
header: &mut QcowHeader,
|
|
||||||
mut feature_table: Option<&mut Vec<(u8, String)>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Extensions start directly after the header
|
|
||||||
f.seek(SeekFrom::Start(header.header_size as u64))
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let ext_type = u32::read_be(f).map_err(Error::ReadingHeader)?;
|
|
||||||
if ext_type == HEADER_EXT_END {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ext_length = u32::read_be(f).map_err(Error::ReadingHeader)?;
|
|
||||||
|
|
||||||
match ext_type {
|
|
||||||
HEADER_EXT_BACKING_FORMAT => {
|
|
||||||
let mut format_bytes = vec![0u8; ext_length as usize];
|
|
||||||
f.read_exact(&mut format_bytes)
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
let format_str = String::from_utf8(format_bytes)
|
|
||||||
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?;
|
|
||||||
if let Some(backing_file) = &mut header.backing_file {
|
|
||||||
backing_file.format = Some(format_str.parse()?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HEADER_EXT_FEATURE_NAME_TABLE if feature_table.is_some() => {
|
|
||||||
const FEATURE_NAME_ENTRY_SIZE: usize = 1 + 1 + 46; // type + bit + name
|
|
||||||
let mut data = vec![0u8; ext_length as usize];
|
|
||||||
f.read_exact(&mut data).map_err(Error::ReadingHeader)?;
|
|
||||||
let table = feature_table.as_mut().unwrap();
|
|
||||||
for entry in data.chunks_exact(FEATURE_NAME_ENTRY_SIZE) {
|
|
||||||
if entry[0] == FEAT_TYPE_INCOMPATIBLE {
|
|
||||||
let bit_number = entry[1];
|
|
||||||
let name_bytes = &entry[2..];
|
|
||||||
let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(46);
|
|
||||||
let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string();
|
|
||||||
table.push((bit_number, name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Skip unknown extension
|
|
||||||
f.seek(SeekFrom::Current(ext_length as i64))
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip to the next 8 byte boundary
|
|
||||||
let padding = (8 - (ext_length % 8)) % 8;
|
|
||||||
f.seek(SeekFrom::Current(padding as i64))
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a QcowHeader from a reference to a file.
|
|
||||||
pub fn new(f: &mut RawFile) -> Result<QcowHeader> {
|
|
||||||
f.rewind().map_err(Error::ReadingHeader)?;
|
|
||||||
let magic = u32::read_be(f).map_err(Error::ReadingHeader)?;
|
|
||||||
if magic != QCOW_MAGIC {
|
|
||||||
return Err(Error::InvalidMagic);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the next u32 from the file.
|
|
||||||
fn read_u32_be(f: &mut RawFile) -> Result<u32> {
|
|
||||||
u32::read_be(f).map_err(Error::ReadingHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the next u64 from the file.
|
|
||||||
fn read_u64_be(f: &mut RawFile) -> Result<u64> {
|
|
||||||
u64::read_be(f).map_err(Error::ReadingHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
let version = read_u32_be(f)?;
|
|
||||||
|
|
||||||
let mut header = QcowHeader {
|
|
||||||
magic,
|
|
||||||
version,
|
|
||||||
backing_file_offset: read_u64_be(f)?,
|
|
||||||
backing_file_size: read_u32_be(f)?,
|
|
||||||
cluster_bits: read_u32_be(f)?,
|
|
||||||
size: read_u64_be(f)?,
|
|
||||||
crypt_method: read_u32_be(f)?,
|
|
||||||
l1_size: read_u32_be(f)?,
|
|
||||||
l1_table_offset: read_u64_be(f)?,
|
|
||||||
refcount_table_offset: read_u64_be(f)?,
|
|
||||||
refcount_table_clusters: read_u32_be(f)?,
|
|
||||||
nb_snapshots: read_u32_be(f)?,
|
|
||||||
snapshots_offset: read_u64_be(f)?,
|
|
||||||
incompatible_features: if version == 2 { 0 } else { read_u64_be(f)? },
|
|
||||||
compatible_features: if version == 2 { 0 } else { read_u64_be(f)? },
|
|
||||||
autoclear_features: if version == 2 { 0 } else { read_u64_be(f)? },
|
|
||||||
refcount_order: if version == 2 {
|
|
||||||
DEFAULT_REFCOUNT_ORDER
|
|
||||||
} else {
|
|
||||||
read_u32_be(f)?
|
|
||||||
},
|
|
||||||
header_size: if version == 2 {
|
|
||||||
V2_BARE_HEADER_SIZE
|
|
||||||
} else {
|
|
||||||
read_u32_be(f)?
|
|
||||||
},
|
|
||||||
compression_type: CompressionType::Zlib,
|
|
||||||
backing_file: None,
|
|
||||||
};
|
|
||||||
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
|
|
||||||
let raw_compression_type = read_u64_be(f)? >> (64 - 8);
|
|
||||||
header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB {
|
|
||||||
Ok(CompressionType::Zlib)
|
|
||||||
} else if raw_compression_type == COMPRESSION_TYPE_ZSTD {
|
|
||||||
Ok(CompressionType::Zstd)
|
|
||||||
} else {
|
|
||||||
Err(Error::UnsupportedCompressionType)
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
if header.backing_file_size > MAX_BACKING_FILE_SIZE {
|
|
||||||
return Err(Error::BackingFileTooLong(header.backing_file_size as usize));
|
|
||||||
}
|
|
||||||
if header.backing_file_offset != 0 {
|
|
||||||
f.seek(SeekFrom::Start(header.backing_file_offset))
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize];
|
|
||||||
f.read_exact(&mut backing_file_name_bytes)
|
|
||||||
.map_err(Error::ReadingHeader)?;
|
|
||||||
let path = String::from_utf8(backing_file_name_bytes)
|
|
||||||
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?;
|
|
||||||
header.backing_file = Some(BackingFileConfig { path, format: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
if version == 3 {
|
|
||||||
// Check for unsupported incompatible features first
|
|
||||||
let features = IncompatFeatures::from_bits_retain(header.incompatible_features);
|
|
||||||
let unsupported = features - IncompatFeatures::SUPPORTED;
|
|
||||||
if !unsupported.is_empty() {
|
|
||||||
// Read extensions only to get feature names for error reporting
|
|
||||||
let mut feature_table = Vec::new();
|
|
||||||
if header.header_size > V3_BARE_HEADER_SIZE {
|
|
||||||
let _ = Self::read_header_extensions(f, &mut header, Some(&mut feature_table));
|
|
||||||
}
|
|
||||||
return Err(Error::UnsupportedFeature(MissingFeatureError::new(
|
|
||||||
unsupported,
|
|
||||||
feature_table,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Features OK, now read extensions normally
|
|
||||||
if header.header_size > V3_BARE_HEADER_SIZE {
|
|
||||||
Self::read_header_extensions(f, &mut header, None)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_decoder(&self) -> Box<dyn Decoder> {
|
|
||||||
match self.compression_type {
|
|
||||||
CompressionType::Zlib => Box::new(ZlibDecoder {}),
|
|
||||||
CompressionType::Zstd => Box::new(ZstdDecoder {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_for_size_and_path(
|
|
||||||
version: u32,
|
|
||||||
size: u64,
|
|
||||||
backing_file: Option<&str>,
|
|
||||||
) -> Result<QcowHeader> {
|
|
||||||
let header_size = if version == 2 {
|
|
||||||
V2_BARE_HEADER_SIZE
|
|
||||||
} else {
|
|
||||||
V3_BARE_HEADER_SIZE + QCOW_EMPTY_HEADER_EXTENSION_SIZE
|
|
||||||
};
|
|
||||||
let cluster_bits: u32 = DEFAULT_CLUSTER_BITS;
|
|
||||||
let cluster_size: u32 = 0x01 << cluster_bits;
|
|
||||||
let max_length: usize = (cluster_size - header_size) as usize;
|
|
||||||
if let Some(path) = backing_file
|
|
||||||
&& path.len() > max_length
|
|
||||||
{
|
|
||||||
return Err(Error::BackingFileTooLong(path.len() - max_length));
|
|
||||||
}
|
|
||||||
|
|
||||||
// L2 blocks are always one cluster long. They contain cluster_size/sizeof(u64) addresses.
|
|
||||||
let entries_per_cluster: u32 = cluster_size / size_of::<u64>() as u32;
|
|
||||||
let num_clusters: u32 = div_round_up_u64(size, u64::from(cluster_size)) as u32;
|
|
||||||
let num_l2_clusters: u32 = div_round_up_u32(num_clusters, entries_per_cluster);
|
|
||||||
let l1_clusters: u32 = div_round_up_u32(num_l2_clusters, entries_per_cluster);
|
|
||||||
let header_clusters = div_round_up_u32(size_of::<QcowHeader>() as u32, cluster_size);
|
|
||||||
Ok(QcowHeader {
|
|
||||||
magic: QCOW_MAGIC,
|
|
||||||
version,
|
|
||||||
backing_file_offset: backing_file.map_or(0, |_| {
|
|
||||||
header_size
|
|
||||||
+ if version == 3 {
|
|
||||||
QCOW_EMPTY_HEADER_EXTENSION_SIZE
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}) as u64,
|
|
||||||
backing_file_size: backing_file.map_or(0, |x| x.len()) as u32,
|
|
||||||
cluster_bits: DEFAULT_CLUSTER_BITS,
|
|
||||||
size,
|
|
||||||
crypt_method: 0,
|
|
||||||
l1_size: num_l2_clusters,
|
|
||||||
l1_table_offset: u64::from(cluster_size),
|
|
||||||
// The refcount table is after l1 + header.
|
|
||||||
refcount_table_offset: u64::from(cluster_size * (l1_clusters + 1)),
|
|
||||||
refcount_table_clusters: {
|
|
||||||
// Pre-allocate enough clusters for the entire refcount table as it must be
|
|
||||||
// continuous in the file. Allocate enough space to refcount all clusters, including
|
|
||||||
// the refcount clusters.
|
|
||||||
let max_refcount_clusters = max_refcount_clusters(
|
|
||||||
DEFAULT_REFCOUNT_ORDER,
|
|
||||||
cluster_size,
|
|
||||||
num_clusters + l1_clusters + num_l2_clusters + header_clusters,
|
|
||||||
) as u32;
|
|
||||||
// The refcount table needs to store the offset of each refcount cluster.
|
|
||||||
div_round_up_u32(
|
|
||||||
max_refcount_clusters * size_of::<u64>() as u32,
|
|
||||||
cluster_size,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
nb_snapshots: 0,
|
|
||||||
snapshots_offset: 0,
|
|
||||||
incompatible_features: 0,
|
|
||||||
compatible_features: 0,
|
|
||||||
autoclear_features: 0,
|
|
||||||
refcount_order: DEFAULT_REFCOUNT_ORDER,
|
|
||||||
header_size,
|
|
||||||
compression_type: CompressionType::Zlib,
|
|
||||||
backing_file: backing_file.map(|path| BackingFileConfig {
|
|
||||||
path: String::from(path),
|
|
||||||
format: None,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the header to `file`.
|
|
||||||
pub fn write_to<F: Write + Seek>(&self, file: &mut F) -> Result<()> {
|
|
||||||
// Writes the next u32 to the file.
|
|
||||||
fn write_u32_be<F: Write>(f: &mut F, value: u32) -> Result<()> {
|
|
||||||
u32::write_be(f, value).map_err(Error::WritingHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes the next u64 to the file.
|
|
||||||
fn write_u64_be<F: Write>(f: &mut F, value: u64) -> Result<()> {
|
|
||||||
u64::write_be(f, value).map_err(Error::WritingHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
write_u32_be(file, self.magic)?;
|
|
||||||
write_u32_be(file, self.version)?;
|
|
||||||
write_u64_be(file, self.backing_file_offset)?;
|
|
||||||
write_u32_be(file, self.backing_file_size)?;
|
|
||||||
write_u32_be(file, self.cluster_bits)?;
|
|
||||||
write_u64_be(file, self.size)?;
|
|
||||||
write_u32_be(file, self.crypt_method)?;
|
|
||||||
write_u32_be(file, self.l1_size)?;
|
|
||||||
write_u64_be(file, self.l1_table_offset)?;
|
|
||||||
write_u64_be(file, self.refcount_table_offset)?;
|
|
||||||
write_u32_be(file, self.refcount_table_clusters)?;
|
|
||||||
write_u32_be(file, self.nb_snapshots)?;
|
|
||||||
write_u64_be(file, self.snapshots_offset)?;
|
|
||||||
|
|
||||||
if self.version == 3 {
|
|
||||||
write_u64_be(file, self.incompatible_features)?;
|
|
||||||
write_u64_be(file, self.compatible_features)?;
|
|
||||||
write_u64_be(file, self.autoclear_features)?;
|
|
||||||
write_u32_be(file, self.refcount_order)?;
|
|
||||||
write_u32_be(file, self.header_size)?;
|
|
||||||
|
|
||||||
if self.header_size > V3_BARE_HEADER_SIZE {
|
|
||||||
write_u64_be(file, 0)?; // no compression
|
|
||||||
}
|
|
||||||
|
|
||||||
write_u32_be(file, 0)?; // header extension type: end of header extension area
|
|
||||||
write_u32_be(file, 0)?; // length of header extension data: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) {
|
|
||||||
if self.backing_file_offset > 0 {
|
|
||||||
file.seek(SeekFrom::Start(self.backing_file_offset))
|
|
||||||
.map_err(Error::WritingHeader)?;
|
|
||||||
}
|
|
||||||
write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the file length by seeking and writing a zero to the last byte. This avoids needing
|
|
||||||
// a `File` instead of anything that implements seek as the `file` argument.
|
|
||||||
// Zeros out the l1 and refcount table clusters.
|
|
||||||
let cluster_size = 0x01u64 << self.cluster_bits;
|
|
||||||
let refcount_blocks_size = u64::from(self.refcount_table_clusters) * cluster_size;
|
|
||||||
file.seek(SeekFrom::Start(
|
|
||||||
self.refcount_table_offset + refcount_blocks_size - 2,
|
|
||||||
))
|
|
||||||
.map_err(Error::WritingHeader)?;
|
|
||||||
file.write(&[0u8]).map_err(Error::WritingHeader)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write only the incompatible_features field to the file at its fixed offset.
|
|
||||||
fn write_incompatible_features<F: Seek + Write>(&self, file: &mut F) -> BlockResult<()> {
|
|
||||||
if self.version != 3 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
file.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?;
|
|
||||||
u64::write_be(file, self.incompatible_features)
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set or clear the dirty bit for QCOW2 v3 images.
|
|
||||||
///
|
|
||||||
/// When `dirty` is true, sets the bit to indicate the image is in use.
|
|
||||||
/// When `dirty` is false, clears the bit to indicate a clean shutdown.
|
|
||||||
pub fn set_dirty_bit<F: Seek + Write + FileSync>(
|
|
||||||
&mut self,
|
|
||||||
file: &mut F,
|
|
||||||
dirty: bool,
|
|
||||||
) -> BlockResult<()> {
|
|
||||||
if self.version == 3 {
|
|
||||||
if dirty {
|
|
||||||
self.incompatible_features |= IncompatFeatures::DIRTY.bits();
|
|
||||||
} else {
|
|
||||||
self.incompatible_features &= !IncompatFeatures::DIRTY.bits();
|
|
||||||
}
|
|
||||||
self.write_incompatible_features(file)?;
|
|
||||||
file.fsync()
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the corrupt bit for QCOW2 v3 images.
|
|
||||||
///
|
|
||||||
/// This marks the image as corrupted. Once set, the image can only be
|
|
||||||
/// opened read-only until repaired.
|
|
||||||
pub fn set_corrupt_bit<F: Seek + Write + FileSync>(&mut self, file: &mut F) -> BlockResult<()> {
|
|
||||||
if self.version == 3 {
|
|
||||||
self.incompatible_features |= IncompatFeatures::CORRUPT.bits();
|
|
||||||
self.write_incompatible_features(file)?;
|
|
||||||
file.fsync()
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_corrupt(&self) -> bool {
|
|
||||||
IncompatFeatures::from_bits_truncate(self.incompatible_features)
|
|
||||||
.contains(IncompatFeatures::CORRUPT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all autoclear feature bits for QCOW2 v3 images.
|
|
||||||
///
|
|
||||||
/// These bits indicate features that can be safely disabled when modified
|
|
||||||
/// by software that doesn't understand them.
|
|
||||||
pub fn clear_autoclear_features<F: Seek + Write + FileSync>(
|
|
||||||
&mut self,
|
|
||||||
file: &mut F,
|
|
||||||
) -> Result<()> {
|
|
||||||
if self.version == 3 && self.autoclear_features != 0 {
|
|
||||||
self.autoclear_features = 0;
|
|
||||||
file.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET))
|
|
||||||
.map_err(Error::WritingHeader)?;
|
|
||||||
u64::write_be(file, 0).map_err(Error::WritingHeader)?;
|
|
||||||
file.fsync().map_err(Error::SyncingHeader)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn max_refcount_clusters(
|
|
||||||
refcount_order: u32,
|
|
||||||
cluster_size: u32,
|
|
||||||
num_clusters: u32,
|
|
||||||
) -> u64 {
|
|
||||||
// Use u64 as the product of the u32 inputs can overflow.
|
|
||||||
let refcount_bits = 0x01u64 << u64::from(refcount_order);
|
|
||||||
let cluster_bits = u64::from(cluster_size) * 8;
|
|
||||||
let for_data = div_round_up_u64(u64::from(num_clusters) * refcount_bits, cluster_bits);
|
|
||||||
let for_refcounts = div_round_up_u64(for_data * refcount_bits, cluster_bits);
|
|
||||||
for_data + for_refcounts
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an Error if the given offset doesn't align to a cluster boundary.
|
|
||||||
pub(super) fn offset_is_cluster_boundary(offset: u64, cluster_bits: u32) -> Result<()> {
|
|
||||||
if offset & ((0x01 << cluster_bits) - 1) != 0 {
|
|
||||||
return Err(Error::InvalidOffset(offset));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,194 +4,34 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
|
||||||
use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
|
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd};
|
use std::os::fd::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use vmm_sys_util::write_zeroes::WriteZeroes;
|
use vmm_sys_util::write_zeroes::WriteZeroes;
|
||||||
|
|
||||||
use super::RawFile;
|
use super::RawFile;
|
||||||
|
|
||||||
// Type aliases for the refcount read/write function pointers
|
|
||||||
type RefcountReader = fn(&mut RawFile, usize) -> io::Result<Vec<u64>>;
|
|
||||||
type RefcountWriter = fn(&mut RawFile, &[u64]) -> io::Result<()>;
|
|
||||||
|
|
||||||
/// Big-endian file access trait.
|
|
||||||
pub(super) trait BeUint: Sized + Copy {
|
|
||||||
fn from_be_slice(bytes: &[u8]) -> u64;
|
|
||||||
fn read_be<R: Read>(r: &mut R) -> io::Result<Self>;
|
|
||||||
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BeUint for u8 {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from_be_slice(bytes: &[u8]) -> u64 {
|
|
||||||
bytes[0] as u64
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
r.read_u8()
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
|
|
||||||
w.write_u8(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BeUint for u16 {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from_be_slice(bytes: &[u8]) -> u64 {
|
|
||||||
u16::from_be_bytes([bytes[0], bytes[1]]) as u64
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
r.read_u16::<BigEndian>()
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
|
|
||||||
w.write_u16::<BigEndian>(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BeUint for u32 {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from_be_slice(bytes: &[u8]) -> u64 {
|
|
||||||
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
r.read_u32::<BigEndian>()
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
|
|
||||||
w.write_u32::<BigEndian>(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BeUint for u64 {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from_be_slice(bytes: &[u8]) -> u64 {
|
|
||||||
u64::from_be_bytes([
|
|
||||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
r.read_u64::<BigEndian>()
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
|
|
||||||
w.write_u64::<BigEndian>(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read byte-aligned refcounts.
|
|
||||||
fn read_refcount<T: BeUint>(file: &mut RawFile, count: usize) -> io::Result<Vec<u64>> {
|
|
||||||
let bytes_per_entry = size_of::<T>();
|
|
||||||
let mut data = vec![0u8; count * bytes_per_entry];
|
|
||||||
file.read_exact(&mut data)?;
|
|
||||||
Ok(data
|
|
||||||
.chunks_exact(bytes_per_entry)
|
|
||||||
.map(T::from_be_slice)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write byte-aligned refcounts.
|
|
||||||
fn write_refcount<T: BeUint + TryFrom<u64>>(file: &mut RawFile, table: &[u64]) -> io::Result<()>
|
|
||||||
where
|
|
||||||
<T as TryFrom<u64>>::Error: Debug,
|
|
||||||
{
|
|
||||||
let bytes_per_entry = size_of::<T>();
|
|
||||||
let mut buffer = BufWriter::with_capacity(table.len() * bytes_per_entry, file);
|
|
||||||
for &val in table {
|
|
||||||
let converted = T::try_from(val).expect("refcount values are validated on increment");
|
|
||||||
T::write_be(&mut buffer, converted)?;
|
|
||||||
}
|
|
||||||
buffer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read sub-byte refcounts. Bit 0 is the least significant bit.
|
|
||||||
fn read_refcount_subbyte<const BITS: usize>(
|
|
||||||
file: &mut RawFile,
|
|
||||||
count: usize,
|
|
||||||
) -> io::Result<Vec<u64>> {
|
|
||||||
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
|
|
||||||
let entries_per_byte = 8 / BITS;
|
|
||||||
let mask = (1u64 << BITS) - 1;
|
|
||||||
let bytes_needed = count.div_ceil(entries_per_byte);
|
|
||||||
let mut bytes = vec![0u8; bytes_needed];
|
|
||||||
file.read_exact(&mut bytes)?;
|
|
||||||
|
|
||||||
let mut table = vec![0u64; count];
|
|
||||||
for (i, val) in table.iter_mut().enumerate() {
|
|
||||||
let byte_idx = i / entries_per_byte;
|
|
||||||
let bit_offset = (i % entries_per_byte) * BITS;
|
|
||||||
*val = (bytes[byte_idx] as u64 >> bit_offset) & mask;
|
|
||||||
}
|
|
||||||
Ok(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write sub-byte refcounts. Bit 0 is the least significant bit.
|
|
||||||
fn write_refcount_subbyte<const BITS: usize>(file: &mut RawFile, table: &[u64]) -> io::Result<()> {
|
|
||||||
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
|
|
||||||
let entries_per_byte = 8 / BITS;
|
|
||||||
let mask = (1u64 << BITS) - 1;
|
|
||||||
let mut buffer = BufWriter::with_capacity(table.len().div_ceil(entries_per_byte), file);
|
|
||||||
|
|
||||||
for chunk in table.chunks(entries_per_byte) {
|
|
||||||
let mut byte = 0u8;
|
|
||||||
for (i, &val) in chunk.iter().enumerate() {
|
|
||||||
let bit_offset = i * BITS;
|
|
||||||
byte |= ((val & mask) << bit_offset) as u8;
|
|
||||||
}
|
|
||||||
buffer.write_u8(byte)?;
|
|
||||||
}
|
|
||||||
buffer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A qcow file. Allows reading/writing clusters and appending clusters.
|
/// A qcow file. Allows reading/writing clusters and appending clusters.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct QcowRawFile {
|
pub struct QcowRawFile {
|
||||||
file: RawFile,
|
file: RawFile,
|
||||||
cluster_size: u64,
|
cluster_size: u64,
|
||||||
cluster_mask: u64,
|
cluster_mask: u64,
|
||||||
refcount_block_entries: u64,
|
|
||||||
read_refcount_fn: RefcountReader,
|
|
||||||
write_refcount_fn: RefcountWriter,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QcowRawFile {
|
impl QcowRawFile {
|
||||||
/// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not
|
/// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not
|
||||||
/// a power of two or refcount_bits is invalid.
|
/// a power of two.
|
||||||
pub fn from(file: RawFile, cluster_size: u64, refcount_bits: u64) -> Option<Self> {
|
pub fn from(file: RawFile, cluster_size: u64) -> Option<Self> {
|
||||||
if !cluster_size.is_power_of_two() {
|
if !cluster_size.is_power_of_two() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (read_refcount_fn, write_refcount_fn): (RefcountReader, RefcountWriter) =
|
|
||||||
match refcount_bits {
|
|
||||||
1 => (read_refcount_subbyte::<1>, write_refcount_subbyte::<1>),
|
|
||||||
2 => (read_refcount_subbyte::<2>, write_refcount_subbyte::<2>),
|
|
||||||
4 => (read_refcount_subbyte::<4>, write_refcount_subbyte::<4>),
|
|
||||||
8 => (read_refcount::<u8>, write_refcount::<u8>),
|
|
||||||
16 => (read_refcount::<u16>, write_refcount::<u16>),
|
|
||||||
32 => (read_refcount::<u32>, write_refcount::<u32>),
|
|
||||||
64 => (read_refcount::<u64>, write_refcount::<u64>),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// For sub-byte refcounts (1,2,4 bits), entries pack multiple per byte
|
|
||||||
let refcount_block_entries = cluster_size * 8 / refcount_bits;
|
|
||||||
|
|
||||||
Some(QcowRawFile {
|
Some(QcowRawFile {
|
||||||
file,
|
file,
|
||||||
cluster_size,
|
cluster_size,
|
||||||
cluster_mask: cluster_size - 1,
|
cluster_mask: cluster_size - 1,
|
||||||
refcount_block_entries,
|
|
||||||
read_refcount_fn,
|
|
||||||
write_refcount_fn,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +86,7 @@ impl QcowRawFile {
|
|||||||
|
|
||||||
for addr in entries {
|
for addr in entries {
|
||||||
let entry = f(self, *addr)?;
|
let entry = f(self, *addr)?;
|
||||||
u64::write_be(&mut buffer, entry)?;
|
buffer.write_u64::<BigEndian>(entry)?;
|
||||||
}
|
}
|
||||||
buffer.flush()?;
|
buffer.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -261,7 +101,7 @@ impl QcowRawFile {
|
|||||||
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
|
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
|
||||||
|
|
||||||
for &entry in entries {
|
for &entry in entries {
|
||||||
u64::write_be(&mut buffer, entry)?;
|
buffer.write_u64::<BigEndian>(entry)?;
|
||||||
}
|
}
|
||||||
buffer.flush()?;
|
buffer.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -269,17 +109,24 @@ impl QcowRawFile {
|
|||||||
|
|
||||||
/// Read a refcount block from the file and returns a Vec containing the block.
|
/// Read a refcount block from the file and returns a Vec containing the block.
|
||||||
/// Always returns a cluster's worth of data.
|
/// Always returns a cluster's worth of data.
|
||||||
#[inline]
|
pub fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u16>> {
|
||||||
pub fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u64>> {
|
let count = self.cluster_size / size_of::<u16>() as u64;
|
||||||
|
let mut table = vec![0; count as usize];
|
||||||
self.file.seek(SeekFrom::Start(offset))?;
|
self.file.seek(SeekFrom::Start(offset))?;
|
||||||
(self.read_refcount_fn)(&mut self.file, self.refcount_block_entries as usize)
|
self.file.read_u16_into::<BigEndian>(&mut table)?;
|
||||||
|
Ok(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a refcount block to the file.
|
/// Writes a refcount block to the file.
|
||||||
#[inline]
|
pub fn write_refcount_block(&mut self, offset: u64, table: &[u16]) -> io::Result<()> {
|
||||||
pub fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> {
|
|
||||||
self.file.seek(SeekFrom::Start(offset))?;
|
self.file.seek(SeekFrom::Start(offset))?;
|
||||||
(self.write_refcount_fn)(&mut self.file, table)
|
let mut buffer = BufWriter::with_capacity(std::mem::size_of_val(table), &mut self.file);
|
||||||
|
|
||||||
|
for count in table {
|
||||||
|
buffer.write_u16::<BigEndian>(*count)?;
|
||||||
|
}
|
||||||
|
buffer.flush()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocates a new cluster at the end of the current file, return the address.
|
/// Allocates a new cluster at the end of the current file, return the address.
|
||||||
@@ -298,11 +145,6 @@ impl QcowRawFile {
|
|||||||
Ok(Some(new_cluster_address))
|
Ok(Some(new_cluster_address))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the underlying file.
|
|
||||||
pub fn file(&self) -> &RawFile {
|
|
||||||
&self.file
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable reference to the underlying file.
|
/// Returns a mutable reference to the underlying file.
|
||||||
pub fn file_mut(&mut self) -> &mut RawFile {
|
pub fn file_mut(&mut self) -> &mut RawFile {
|
||||||
&mut self.file
|
&mut self.file
|
||||||
@@ -349,9 +191,6 @@ impl Clone for QcowRawFile {
|
|||||||
file: self.file.try_clone().expect("QcowRawFile cloning failed"),
|
file: self.file.try_clone().expect("QcowRawFile cloning failed"),
|
||||||
cluster_size: self.cluster_size,
|
cluster_size: self.cluster_size,
|
||||||
cluster_mask: self.cluster_mask,
|
cluster_mask: self.cluster_mask,
|
||||||
refcount_block_entries: self.refcount_block_entries,
|
|
||||||
read_refcount_fn: self.read_refcount_fn,
|
|
||||||
write_refcount_fn: self.write_refcount_fn,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,9 +200,3 @@ impl AsRawFd for QcowRawFile {
|
|||||||
self.file.as_raw_fd()
|
self.file.as_raw_fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsFd for QcowRawFile {
|
|
||||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
|
||||||
self.file.as_fd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,15 +11,14 @@
|
|||||||
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
||||||
use std::fs::{File, Metadata};
|
use std::fs::{File, Metadata};
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
use std::os::fd::{AsFd, BorrowedFd};
|
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
use vmm_sys_util::file_traits::FileSync;
|
use libc::c_void;
|
||||||
use vmm_sys_util::seek_hole::SeekHole;
|
use vmm_sys_util::seek_hole::SeekHole;
|
||||||
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
||||||
|
|
||||||
use crate::{BlockBackend, query_device_size};
|
use crate::BlockBackend;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RawFile {
|
pub struct RawFile {
|
||||||
@@ -38,7 +37,14 @@ fn is_valid_alignment(fd: RawFd, alignment: usize) -> bool {
|
|||||||
assert!(!ptr.is_null());
|
assert!(!ptr.is_null());
|
||||||
|
|
||||||
// SAFETY: FFI call
|
// SAFETY: FFI call
|
||||||
let ret = unsafe { ::libc::pread(fd, ptr.cast(), alignment, alignment.try_into().unwrap()) };
|
let ret = unsafe {
|
||||||
|
::libc::pread(
|
||||||
|
fd,
|
||||||
|
ptr as *mut c_void,
|
||||||
|
alignment,
|
||||||
|
alignment.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// SAFETY: ptr was allocated by alloc_zeroed with layout
|
// SAFETY: ptr was allocated by alloc_zeroed with layout
|
||||||
unsafe { dealloc(ptr, layout) };
|
unsafe { dealloc(ptr, layout) };
|
||||||
@@ -116,21 +122,6 @@ impl RawFile {
|
|||||||
pub fn is_direct(&self) -> bool {
|
pub fn is_direct(&self) -> bool {
|
||||||
self.direct_io
|
self.direct_io
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alignment(&self) -> usize {
|
|
||||||
self.alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the file was opened with write access.
|
|
||||||
pub fn is_writable(&self) -> bool {
|
|
||||||
// SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor
|
|
||||||
let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) };
|
|
||||||
if flags < 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let access_mode = flags & libc::O_ACCMODE;
|
|
||||||
access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for RawFile {
|
impl Read for RawFile {
|
||||||
@@ -179,7 +170,7 @@ impl Read for RawFile {
|
|||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
::libc::pread64(
|
::libc::pread64(
|
||||||
self.file.as_raw_fd(),
|
self.file.as_raw_fd(),
|
||||||
tmp_buf.as_mut_ptr().cast(),
|
tmp_buf.as_mut_ptr() as *mut c_void,
|
||||||
tmp_buf.len(),
|
tmp_buf.len(),
|
||||||
rounded_pos.try_into().unwrap(),
|
rounded_pos.try_into().unwrap(),
|
||||||
)
|
)
|
||||||
@@ -259,7 +250,7 @@ impl Write for RawFile {
|
|||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
::libc::pread64(
|
::libc::pread64(
|
||||||
self.file.as_raw_fd(),
|
self.file.as_raw_fd(),
|
||||||
tmp_buf.as_mut_ptr().cast(),
|
tmp_buf.as_mut_ptr() as *mut c_void,
|
||||||
tmp_buf.len(),
|
tmp_buf.len(),
|
||||||
rounded_pos.try_into().unwrap(),
|
rounded_pos.try_into().unwrap(),
|
||||||
)
|
)
|
||||||
@@ -278,7 +269,7 @@ impl Write for RawFile {
|
|||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
::libc::pwrite64(
|
::libc::pwrite64(
|
||||||
self.file.as_raw_fd(),
|
self.file.as_raw_fd(),
|
||||||
tmp_buf.as_ptr().cast(),
|
tmp_buf.as_ptr() as *const c_void,
|
||||||
tmp_buf.len(),
|
tmp_buf.len(),
|
||||||
rounded_pos.try_into().unwrap(),
|
rounded_pos.try_into().unwrap(),
|
||||||
)
|
)
|
||||||
@@ -336,12 +327,6 @@ impl PunchHole for RawFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSync for RawFile {
|
|
||||||
fn fsync(&mut self) -> std::io::Result<()> {
|
|
||||||
self.file.fsync()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SeekHole for RawFile {
|
impl SeekHole for RawFile {
|
||||||
fn seek_hole(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
|
fn seek_hole(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
|
||||||
match self.file.seek_hole(offset) {
|
match self.file.seek_hole(offset) {
|
||||||
@@ -370,15 +355,11 @@ impl SeekHole for RawFile {
|
|||||||
|
|
||||||
impl BlockBackend for RawFile {
|
impl BlockBackend for RawFile {
|
||||||
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
|
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
|
||||||
Ok(query_device_size(&self.file)
|
Ok(self.metadata().map_err(crate::Error::RawFileError)?.len())
|
||||||
.map_err(crate::Error::RawFileError)?
|
|
||||||
.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
|
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
|
||||||
Ok(query_device_size(&self.file)
|
Ok(self.metadata().map_err(crate::Error::RawFileError)?.len())
|
||||||
.map_err(crate::Error::RawFileError)?
|
|
||||||
.1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,9 +379,3 @@ impl AsRawFd for RawFile {
|
|||||||
self.file.as_raw_fd()
|
self.file.as_raw_fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsFd for RawFile {
|
|
||||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
|
||||||
self.file.as_fd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ pub enum Error {
|
|||||||
/// `InvalidIndex` - Address requested isn't within the range of the disk.
|
/// `InvalidIndex` - Address requested isn't within the range of the disk.
|
||||||
#[error("Address requested is not within the range of the disk")]
|
#[error("Address requested is not within the range of the disk")]
|
||||||
InvalidIndex,
|
InvalidIndex,
|
||||||
/// `RefblockUnaligned` - Refcount block offset is not cluster aligned.
|
|
||||||
#[error("Refcount block offset {0:#x} is not cluster aligned")]
|
|
||||||
RefblockUnaligned(u64),
|
|
||||||
/// `NeedCluster` - Handle this error by reading the cluster and calling the function again.
|
/// `NeedCluster` - Handle this error by reading the cluster and calling the function again.
|
||||||
#[error("Cluster with addr={0} needs to be read")]
|
#[error("Cluster with addr={0} needs to be read")]
|
||||||
NeedCluster(u64),
|
NeedCluster(u64),
|
||||||
@@ -32,13 +29,6 @@ pub enum Error {
|
|||||||
/// `ReadingRefCounts` - Error reading the file into the refcount cache.
|
/// `ReadingRefCounts` - Error reading the file into the refcount cache.
|
||||||
#[error("Failed to read the file into the refcount cache")]
|
#[error("Failed to read the file into the refcount cache")]
|
||||||
ReadingRefCounts(#[source] io::Error),
|
ReadingRefCounts(#[source] io::Error),
|
||||||
/// `RefcountOverflow` - Refcount value exceeds maximum for the refcount width.
|
|
||||||
#[error("Refcount value {value} exceeds {refcount_bits}-bit max ({max})")]
|
|
||||||
RefcountOverflow {
|
|
||||||
value: u64,
|
|
||||||
max: u64,
|
|
||||||
refcount_bits: u64,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
@@ -48,19 +38,16 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
pub struct RefCount {
|
pub struct RefCount {
|
||||||
ref_table: VecCache<u64>,
|
ref_table: VecCache<u64>,
|
||||||
refcount_table_offset: u64,
|
refcount_table_offset: u64,
|
||||||
refblock_cache: CacheMap<VecCache<u64>>,
|
refblock_cache: CacheMap<VecCache<u16>>,
|
||||||
refcount_block_entries: u64, // number of refcounts in a cluster.
|
refcount_block_entries: u64, // number of refcounts in a cluster.
|
||||||
cluster_size: u64,
|
cluster_size: u64,
|
||||||
max_valid_cluster_offset: u64,
|
max_valid_cluster_offset: u64,
|
||||||
max_refcount: u64, // maximum refcount value for this image's refcount_order
|
|
||||||
refcount_bits: u64, // number of bits per refcount entry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefCount {
|
impl RefCount {
|
||||||
/// Creates a `RefCount` from `file`, reading the refcount table from `refcount_table_offset`.
|
/// Creates a `RefCount` from `file`, reading the refcount table from `refcount_table_offset`.
|
||||||
/// `refcount_table_entries` specifies the number of refcount blocks used by this image.
|
/// `refcount_table_entries` specifies the number of refcount blocks used by this image.
|
||||||
/// `refcount_block_entries` indicates the number of refcounts in each refcount block.
|
/// `refcount_block_entries` indicates the number of refcounts in each refcount block.
|
||||||
/// `refcount_bits` is the number of bits per refcount (1, 2, 4, 8, 16, 32, or 64).
|
|
||||||
/// Each refcount table entry points to a refcount block.
|
/// Each refcount table entry points to a refcount block.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
raw_file: &mut QcowRawFile,
|
raw_file: &mut QcowRawFile,
|
||||||
@@ -68,7 +55,6 @@ impl RefCount {
|
|||||||
refcount_table_entries: u64,
|
refcount_table_entries: u64,
|
||||||
refcount_block_entries: u64,
|
refcount_block_entries: u64,
|
||||||
cluster_size: u64,
|
cluster_size: u64,
|
||||||
refcount_bits: u64,
|
|
||||||
) -> io::Result<RefCount> {
|
) -> io::Result<RefCount> {
|
||||||
let ref_table = VecCache::from_vec(raw_file.read_pointer_table(
|
let ref_table = VecCache::from_vec(raw_file.read_pointer_table(
|
||||||
refcount_table_offset,
|
refcount_table_offset,
|
||||||
@@ -77,11 +63,6 @@ impl RefCount {
|
|||||||
)?);
|
)?);
|
||||||
let max_valid_cluster_index = (ref_table.len() as u64) * refcount_block_entries - 1;
|
let max_valid_cluster_index = (ref_table.len() as u64) * refcount_block_entries - 1;
|
||||||
let max_valid_cluster_offset = max_valid_cluster_index * cluster_size;
|
let max_valid_cluster_offset = max_valid_cluster_index * cluster_size;
|
||||||
let max_refcount = if refcount_bits >= 64 {
|
|
||||||
u64::MAX
|
|
||||||
} else {
|
|
||||||
(1u64 << refcount_bits) - 1
|
|
||||||
};
|
|
||||||
Ok(RefCount {
|
Ok(RefCount {
|
||||||
ref_table,
|
ref_table,
|
||||||
refcount_table_offset,
|
refcount_table_offset,
|
||||||
@@ -89,8 +70,6 @@ impl RefCount {
|
|||||||
refcount_block_entries,
|
refcount_block_entries,
|
||||||
cluster_size,
|
cluster_size,
|
||||||
max_valid_cluster_offset,
|
max_valid_cluster_offset,
|
||||||
max_refcount,
|
|
||||||
refcount_bits,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,17 +92,9 @@ impl RefCount {
|
|||||||
&mut self,
|
&mut self,
|
||||||
raw_file: &mut QcowRawFile,
|
raw_file: &mut QcowRawFile,
|
||||||
cluster_address: u64,
|
cluster_address: u64,
|
||||||
refcount: u64,
|
refcount: u16,
|
||||||
mut new_cluster: Option<(u64, VecCache<u64>)>,
|
mut new_cluster: Option<(u64, VecCache<u16>)>,
|
||||||
) -> Result<Option<u64>> {
|
) -> Result<Option<u64>> {
|
||||||
if refcount > self.max_refcount {
|
|
||||||
return Err(Error::RefcountOverflow {
|
|
||||||
value: refcount,
|
|
||||||
max: self.max_refcount,
|
|
||||||
refcount_bits: self.refcount_bits,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let (table_index, block_index) = self.get_refcount_index(cluster_address);
|
let (table_index, block_index) = self.get_refcount_index(cluster_address);
|
||||||
|
|
||||||
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||||
@@ -199,15 +170,12 @@ impl RefCount {
|
|||||||
&mut self,
|
&mut self,
|
||||||
raw_file: &mut QcowRawFile,
|
raw_file: &mut QcowRawFile,
|
||||||
address: u64,
|
address: u64,
|
||||||
) -> Result<u64> {
|
) -> Result<u16> {
|
||||||
let (table_index, block_index) = self.get_refcount_index(address);
|
let (table_index, block_index) = self.get_refcount_index(address);
|
||||||
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||||
if block_addr_disk == 0 {
|
if block_addr_disk == 0 {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
if block_addr_disk & (self.cluster_size - 1) != 0 {
|
|
||||||
return Err(Error::RefblockUnaligned(block_addr_disk));
|
|
||||||
}
|
|
||||||
if !self.refblock_cache.contains_key(table_index) {
|
if !self.refblock_cache.contains_key(table_index) {
|
||||||
let table = VecCache::from_vec(
|
let table = VecCache::from_vec(
|
||||||
raw_file
|
raw_file
|
||||||
@@ -234,7 +202,7 @@ impl RefCount {
|
|||||||
&mut self,
|
&mut self,
|
||||||
raw_file: &mut QcowRawFile,
|
raw_file: &mut QcowRawFile,
|
||||||
table_index: usize,
|
table_index: usize,
|
||||||
) -> Result<Option<&[u64]>> {
|
) -> Result<Option<&[u16]>> {
|
||||||
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||||
if block_addr_disk == 0 {
|
if block_addr_disk == 0 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
// Copyright 2018 The Chromium OS Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE-BSD-3-Clause file.
|
|
||||||
//
|
|
||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
//! Pure helper functions and constants for QCOW2 L1/L2 table entry
|
|
||||||
//! manipulation and integer arithmetic. Shared across the `qcow` submodules.
|
|
||||||
|
|
||||||
/// Nesting depth limit for disk formats that can open other disk files.
|
|
||||||
pub(crate) const MAX_NESTING_DEPTH: u32 = 10;
|
|
||||||
|
|
||||||
// bits 0-8 and 56-63 are reserved.
|
|
||||||
pub(super) const L1_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
|
|
||||||
pub(super) const L2_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
|
|
||||||
// Flags
|
|
||||||
pub(super) const ZERO_FLAG: u64 = 1 << 0;
|
|
||||||
pub(super) const COMPRESSED_FLAG: u64 = 1 << 62;
|
|
||||||
pub(super) const COMPRESSED_SECTOR_SIZE: u64 = 512;
|
|
||||||
pub(super) const CLUSTER_USED_FLAG: u64 = 1 << 63;
|
|
||||||
|
|
||||||
/// Check if L2 entry is empty (unallocated).
|
|
||||||
pub(super) fn l2_entry_is_empty(l2_entry: u64) -> bool {
|
|
||||||
l2_entry == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check bit 0 - only valid for standard clusters.
|
|
||||||
pub(super) fn l2_entry_is_zero(l2_entry: u64) -> bool {
|
|
||||||
l2_entry & ZERO_FLAG != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if L2 entry refers to a compressed cluster.
|
|
||||||
pub(super) fn l2_entry_is_compressed(l2_entry: u64) -> bool {
|
|
||||||
l2_entry & COMPRESSED_FLAG != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get file offset and size of compressed cluster data.
|
|
||||||
pub(super) fn l2_entry_compressed_cluster_layout(l2_entry: u64, cluster_bits: u32) -> (u64, usize) {
|
|
||||||
let compressed_size_shift = 62 - (cluster_bits - 8);
|
|
||||||
let compressed_size_mask = (1 << (cluster_bits - 8)) - 1;
|
|
||||||
let compressed_cluster_addr = l2_entry & ((1 << compressed_size_shift) - 1);
|
|
||||||
let nsectors = (l2_entry >> compressed_size_shift & compressed_size_mask) + 1;
|
|
||||||
let compressed_cluster_size = ((nsectors * COMPRESSED_SECTOR_SIZE)
|
|
||||||
- (compressed_cluster_addr & (COMPRESSED_SECTOR_SIZE - 1)))
|
|
||||||
as usize;
|
|
||||||
(compressed_cluster_addr, compressed_cluster_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get file offset of standard (non-compressed) cluster.
|
|
||||||
pub(super) fn l2_entry_std_cluster_addr(l2_entry: u64) -> u64 {
|
|
||||||
l2_entry & L2_TABLE_OFFSET_MASK
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make L2 entry for standard (non-compressed) cluster.
|
|
||||||
pub(super) fn l2_entry_make_std(cluster_addr: u64) -> u64 {
|
|
||||||
(cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make L2 entry for preallocated zero cluster.
|
|
||||||
pub(super) fn l2_entry_make_zero(cluster_addr: u64) -> u64 {
|
|
||||||
(cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG | ZERO_FLAG
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make L1 entry with optional flags.
|
|
||||||
pub(super) fn l1_entry_make(cluster_addr: u64, refcount_is_one: bool) -> u64 {
|
|
||||||
(cluster_addr & L1_TABLE_OFFSET_MASK) | (refcount_is_one as u64 * CLUSTER_USED_FLAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ceiling of the division of `dividend`/`divisor`.
|
|
||||||
pub(super) fn div_round_up_u32(dividend: u32, divisor: u32) -> u32 {
|
|
||||||
dividend / divisor + u32::from(!dividend.is_multiple_of(divisor))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ceiling of the division of `dividend`/`divisor`.
|
|
||||||
pub(super) fn div_round_up_u64(dividend: u64, divisor: u64) -> u64 {
|
|
||||||
dividend / divisor + u64::from(!dividend.is_multiple_of(divisor))
|
|
||||||
}
|
|
||||||
@@ -62,21 +62,6 @@ impl<T: 'static + Copy + Default> VecCache<T> {
|
|||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.vec.len()
|
self.vec.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extends the cache capacity to `new_len` elements.
|
|
||||||
///
|
|
||||||
/// No-op if `new_len <= self.len()`. Allocates a new buffer, copies
|
|
||||||
/// existing data, and fills new elements with default values.
|
|
||||||
/// Marks the cache as dirty.
|
|
||||||
pub fn extend(&mut self, new_len: usize) {
|
|
||||||
if new_len <= self.vec.len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut new_vec = vec![Default::default(); new_len];
|
|
||||||
new_vec[..self.vec.len()].copy_from_slice(&self.vec);
|
|
||||||
self.vec = new_vec.into_boxed_slice();
|
|
||||||
self.dirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static + Copy + Default> Cacheable for VecCache<T> {
|
impl<T: 'static + Copy + Default> Cacheable for VecCache<T> {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,446 +0,0 @@
|
|||||||
// Copyright © 2021 Intel Corporation
|
|
||||||
//
|
|
||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
//! Shared helpers for QCOW2 sync and async backends.
|
|
||||||
//!
|
|
||||||
//! Position-independent I/O (`pread_exact`, `pwrite_all`) and iovec
|
|
||||||
//! scatter/gather helpers used by both `qcow_sync` and `qcow_async`.
|
|
||||||
|
|
||||||
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
|
||||||
use std::cmp::min;
|
|
||||||
use std::os::fd::RawFd;
|
|
||||||
use std::{io, ptr, slice};
|
|
||||||
|
|
||||||
use crate::qcow::decoder::Decoder;
|
|
||||||
|
|
||||||
// -- Position independent I/O helpers --
|
|
||||||
//
|
|
||||||
// Duplicated file descriptors share the kernel file description and thus the
|
|
||||||
// file position. Using seek then read from multiple queues races on that
|
|
||||||
// shared position. pread64 and pwrite64 are atomic and never touch the position.
|
|
||||||
|
|
||||||
/// Read exactly the requested bytes at offset, looping on short reads.
|
|
||||||
pub fn pread_exact(fd: RawFd, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
|
||||||
let mut total = 0usize;
|
|
||||||
while total < buf.len() {
|
|
||||||
// SAFETY: buf and fd are valid for the lifetime of the call.
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::pread64(
|
|
||||||
fd,
|
|
||||||
buf[total..].as_mut_ptr().cast(),
|
|
||||||
buf.len() - total,
|
|
||||||
(offset + total as u64) as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ret < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
if ret == 0 {
|
|
||||||
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
|
|
||||||
}
|
|
||||||
total += ret as usize;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate a buffer and pread exactly `len` bytes at `offset`.
|
|
||||||
pub fn pread_alloc(fd: RawFd, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
|
||||||
let mut buf = vec![0u8; len];
|
|
||||||
pread_exact(fd, &mut buf, offset)?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decompress a full QCOW2 cluster from compressed data.
|
|
||||||
///
|
|
||||||
/// Returns a `cluster_size` byte buffer with the decompressed cluster
|
|
||||||
/// content. Fails if the decoder does not produce exactly `cluster_size`
|
|
||||||
/// bytes.
|
|
||||||
pub fn decompress_cluster(
|
|
||||||
compressed: &[u8],
|
|
||||||
cluster_size: usize,
|
|
||||||
decoder: &dyn Decoder,
|
|
||||||
) -> io::Result<Vec<u8>> {
|
|
||||||
let mut decompressed = vec![0u8; cluster_size];
|
|
||||||
let n = decoder
|
|
||||||
.decode(compressed, &mut decompressed)
|
|
||||||
.map_err(|_| io::Error::from_raw_os_error(libc::EIO))?;
|
|
||||||
if n != cluster_size {
|
|
||||||
return Err(io::Error::from_raw_os_error(libc::EIO));
|
|
||||||
}
|
|
||||||
Ok(decompressed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write all bytes to fd at offset, looping on short writes.
|
|
||||||
pub fn pwrite_all(fd: RawFd, buf: &[u8], offset: u64) -> io::Result<()> {
|
|
||||||
let mut total = 0usize;
|
|
||||||
while total < buf.len() {
|
|
||||||
// SAFETY: buf and fd are valid for the lifetime of the call.
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::pwrite64(
|
|
||||||
fd,
|
|
||||||
buf[total..].as_ptr().cast(),
|
|
||||||
buf.len() - total,
|
|
||||||
(offset + total as u64) as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ret < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
if ret == 0 {
|
|
||||||
return Err(io::Error::other("pwrite64 wrote 0 bytes"));
|
|
||||||
}
|
|
||||||
total += ret as usize;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RAII wrapper for an aligned heap buffer required by O_DIRECT.
|
|
||||||
pub struct AlignedBuf {
|
|
||||||
ptr: *mut u8,
|
|
||||||
layout: Layout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlignedBuf {
|
|
||||||
pub fn new(size: usize, alignment: usize) -> io::Result<Self> {
|
|
||||||
let size = size.max(1).next_multiple_of(alignment);
|
|
||||||
let layout = Layout::from_size_align(size, alignment)
|
|
||||||
.map_err(|e| io::Error::other(format!("invalid aligned layout: {e}")))?;
|
|
||||||
// SAFETY: layout has non-zero size.
|
|
||||||
let ptr = unsafe { alloc_zeroed(layout) };
|
|
||||||
if ptr.is_null() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::OutOfMemory,
|
|
||||||
"aligned allocation failed",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(AlignedBuf { ptr, layout })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_mut_slice(&mut self, len: usize) -> &mut [u8] {
|
|
||||||
let len = len.min(self.layout.size());
|
|
||||||
// SAFETY: ptr is valid for layout.size() bytes; len <= layout.size().
|
|
||||||
unsafe { slice::from_raw_parts_mut(self.ptr, len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_slice(&self, len: usize) -> &[u8] {
|
|
||||||
let len = len.min(self.layout.size());
|
|
||||||
// SAFETY: ptr is valid for layout.size() bytes; len <= layout.size().
|
|
||||||
unsafe { slice::from_raw_parts(self.ptr, len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn layout(&self) -> &Layout {
|
|
||||||
&self.layout
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn ptr(&self) -> *const u8 {
|
|
||||||
self.ptr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for AlignedBuf {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// SAFETY: ptr was allocated by alloc_zeroed with self.layout.
|
|
||||||
unsafe { dealloc(self.ptr, self.layout) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read into `buf` via an aligned bounce buffer when O_DIRECT requires it.
|
|
||||||
pub fn aligned_pread(fd: RawFd, buf: &mut [u8], offset: u64, alignment: usize) -> io::Result<()> {
|
|
||||||
if alignment == 0
|
|
||||||
|| ((buf.as_ptr() as usize).is_multiple_of(alignment)
|
|
||||||
&& buf.len().is_multiple_of(alignment)
|
|
||||||
&& (offset as usize).is_multiple_of(alignment))
|
|
||||||
{
|
|
||||||
return pread_exact(fd, buf, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aligned_offset = offset & !(alignment as u64 - 1);
|
|
||||||
let head = (offset - aligned_offset) as usize;
|
|
||||||
let aligned_len = (head + buf.len()).next_multiple_of(alignment);
|
|
||||||
let mut bounce = AlignedBuf::new(aligned_len, alignment)?;
|
|
||||||
pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?;
|
|
||||||
buf.copy_from_slice(&bounce.as_slice(aligned_len)[head..head + buf.len()]);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `buf` via an aligned bounce buffer when O_DIRECT requires it.
|
|
||||||
pub fn aligned_pwrite(fd: RawFd, buf: &[u8], offset: u64, alignment: usize) -> io::Result<()> {
|
|
||||||
if alignment == 0
|
|
||||||
|| ((buf.as_ptr() as usize).is_multiple_of(alignment)
|
|
||||||
&& buf.len().is_multiple_of(alignment)
|
|
||||||
&& (offset as usize).is_multiple_of(alignment))
|
|
||||||
{
|
|
||||||
return pwrite_all(fd, buf, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aligned_offset = offset & !(alignment as u64 - 1);
|
|
||||||
let head = (offset - aligned_offset) as usize;
|
|
||||||
let aligned_len = (head + buf.len()).next_multiple_of(alignment);
|
|
||||||
let mut bounce = AlignedBuf::new(aligned_len, alignment)?;
|
|
||||||
|
|
||||||
// Read-modify-write: read the existing aligned region, overlay our data.
|
|
||||||
pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?;
|
|
||||||
bounce.as_mut_slice(aligned_len)[head..head + buf.len()].copy_from_slice(buf);
|
|
||||||
pwrite_all(fd, bounce.as_slice(aligned_len), aligned_offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- iovec helper functions --
|
|
||||||
//
|
|
||||||
// Operate on the iovec array as a flat byte stream.
|
|
||||||
|
|
||||||
/// Copy data into iovecs starting at the given byte offset.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
|
|
||||||
pub unsafe fn scatter_to_iovecs(iovecs: &[libc::iovec], start: usize, data: &[u8]) {
|
|
||||||
let mut remaining = data;
|
|
||||||
let mut pos = 0usize;
|
|
||||||
for iov in iovecs {
|
|
||||||
let iov_end = pos + iov.iov_len;
|
|
||||||
if iov_end <= start || remaining.is_empty() {
|
|
||||||
pos = iov_end;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let iov_start = start.saturating_sub(pos);
|
|
||||||
let available = iov.iov_len - iov_start;
|
|
||||||
let count = min(available, remaining.len());
|
|
||||||
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
||||||
unsafe {
|
|
||||||
let dst = iov.iov_base.cast::<u8>().add(iov_start);
|
|
||||||
ptr::copy_nonoverlapping(remaining.as_ptr(), dst, count);
|
|
||||||
}
|
|
||||||
remaining = &remaining[count..];
|
|
||||||
if remaining.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pos = iov_end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zero fill iovecs starting at the given byte offset for the given length.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
|
|
||||||
pub unsafe fn zero_fill_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) {
|
|
||||||
let mut remaining = len;
|
|
||||||
let mut pos = 0usize;
|
|
||||||
for iov in iovecs {
|
|
||||||
let iov_end = pos + iov.iov_len;
|
|
||||||
if iov_end <= start || remaining == 0 {
|
|
||||||
pos = iov_end;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let iov_start = start.saturating_sub(pos);
|
|
||||||
let available = iov.iov_len - iov_start;
|
|
||||||
let count = min(available, remaining);
|
|
||||||
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
||||||
unsafe {
|
|
||||||
let dst = iov.iov_base.cast::<u8>().add(iov_start);
|
|
||||||
ptr::write_bytes(dst, 0, count);
|
|
||||||
}
|
|
||||||
remaining -= count;
|
|
||||||
if remaining == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pos = iov_end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gather bytes from iovecs starting at the given byte offset into `dst`.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// Caller must ensure iovecs point to valid, readable memory of sufficient size.
|
|
||||||
pub unsafe fn gather_from_iovecs_into(iovecs: &[libc::iovec], start: usize, dst: &mut [u8]) {
|
|
||||||
let len = dst.len();
|
|
||||||
let mut written = 0usize;
|
|
||||||
let mut pos = 0usize;
|
|
||||||
for iov in iovecs {
|
|
||||||
let iov_end = pos + iov.iov_len;
|
|
||||||
if iov_end <= start || written == len {
|
|
||||||
pos = iov_end;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let iov_start = start.saturating_sub(pos);
|
|
||||||
let available = iov.iov_len - iov_start;
|
|
||||||
let count = min(available, len - written);
|
|
||||||
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
||||||
unsafe {
|
|
||||||
let src = iov.iov_base.cast::<u8>().add(iov_start);
|
|
||||||
ptr::copy_nonoverlapping(src, dst.as_mut_ptr().add(written), count);
|
|
||||||
}
|
|
||||||
written += count;
|
|
||||||
if written == len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pos = iov_end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gather bytes from iovecs starting at the given byte offset into a Vec.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// Caller must ensure iovecs point to valid, readable memory of sufficient size.
|
|
||||||
pub unsafe fn gather_from_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) -> Vec<u8> {
|
|
||||||
let mut result = vec![0u8; len];
|
|
||||||
// SAFETY: caller guarantees iovecs are valid; result has len bytes.
|
|
||||||
unsafe { gather_from_iovecs_into(iovecs, start, &mut result) };
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod unit_tests {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
|
||||||
use std::os::unix::fs::FileExt;
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
|
||||||
use flate2::Compression;
|
|
||||||
use flate2::write::DeflateEncoder;
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::{decompress_cluster, pread_alloc};
|
|
||||||
use crate::qcow::decoder::ZlibDecoder;
|
|
||||||
|
|
||||||
const COMPRESSED_FLAG: u64 = 1 << 62;
|
|
||||||
const CLUSTER_USED_FLAG: u64 = 1 << 63;
|
|
||||||
const COMPRESSED_SECTOR_SIZE: u64 = 512;
|
|
||||||
|
|
||||||
const HEADER_CLUSTER_BITS_OFFSET: u64 = 20;
|
|
||||||
const HEADER_L1_SIZE_OFFSET: u64 = 36;
|
|
||||||
const HEADER_L1_TABLE_OFFSET: u64 = 40;
|
|
||||||
|
|
||||||
const L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00;
|
|
||||||
|
|
||||||
fn make_compressed_l2_entry(host_offset: u64, compressed_len: usize, cluster_bits: u32) -> u64 {
|
|
||||||
let compressed_size_shift = 62 - (cluster_bits - 8);
|
|
||||||
let intra_sector_offset = host_offset & (COMPRESSED_SECTOR_SIZE - 1);
|
|
||||||
let total_bytes = compressed_len as u64 + intra_sector_offset;
|
|
||||||
let nsectors = total_bytes.div_ceil(COMPRESSED_SECTOR_SIZE);
|
|
||||||
let addr_part = host_offset & ((1 << compressed_size_shift) - 1);
|
|
||||||
let size_part = (nsectors - 1) << compressed_size_shift;
|
|
||||||
COMPRESSED_FLAG | size_part | addr_part
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compress every allocated cluster in a QCOW2 image file in place.
|
|
||||||
///
|
|
||||||
/// Walks L1 -> L2 tables, compresses each standard cluster with raw
|
|
||||||
/// deflate, appends the compressed payload at the end of the file,
|
|
||||||
/// and rewrites the L2 entry with the compressed layout.
|
|
||||||
pub fn compress_allocated_clusters(file: &mut File) {
|
|
||||||
file.seek(SeekFrom::Start(HEADER_CLUSTER_BITS_OFFSET))
|
|
||||||
.unwrap();
|
|
||||||
let cluster_bits = file.read_u32::<BigEndian>().unwrap();
|
|
||||||
let cluster_size = 1u64 << cluster_bits;
|
|
||||||
|
|
||||||
file.seek(SeekFrom::Start(HEADER_L1_SIZE_OFFSET)).unwrap();
|
|
||||||
let l1_size = file.read_u32::<BigEndian>().unwrap();
|
|
||||||
|
|
||||||
file.seek(SeekFrom::Start(HEADER_L1_TABLE_OFFSET)).unwrap();
|
|
||||||
let l1_table_offset = file.read_u64::<BigEndian>().unwrap();
|
|
||||||
|
|
||||||
let entries_per_l2 = cluster_size / 8;
|
|
||||||
|
|
||||||
let mut append_offset = file.seek(SeekFrom::End(0)).unwrap();
|
|
||||||
append_offset = (append_offset + 511) & !511;
|
|
||||||
|
|
||||||
for l1_idx in 0..l1_size as u64 {
|
|
||||||
let l1_entry_offset = l1_table_offset + l1_idx * 8;
|
|
||||||
file.seek(SeekFrom::Start(l1_entry_offset)).unwrap();
|
|
||||||
let l1_entry = file.read_u64::<BigEndian>().unwrap();
|
|
||||||
|
|
||||||
let l2_table_addr = l1_entry & L1_L2_ADDR_MASK;
|
|
||||||
if l2_table_addr == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for l2_idx in 0..entries_per_l2 {
|
|
||||||
let l2_entry_offset = l2_table_addr + l2_idx * 8;
|
|
||||||
file.seek(SeekFrom::Start(l2_entry_offset)).unwrap();
|
|
||||||
let l2_entry = file.read_u64::<BigEndian>().unwrap();
|
|
||||||
|
|
||||||
if l2_entry & CLUSTER_USED_FLAG == 0 || l2_entry & COMPRESSED_FLAG != 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let host_cluster_addr = l2_entry & L1_L2_ADDR_MASK;
|
|
||||||
if host_cluster_addr == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cluster_data = vec![0u8; cluster_size as usize];
|
|
||||||
file.seek(SeekFrom::Start(host_cluster_addr)).unwrap();
|
|
||||||
file.read_exact(&mut cluster_data).unwrap();
|
|
||||||
|
|
||||||
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
|
|
||||||
encoder.write_all(&cluster_data).unwrap();
|
|
||||||
let compressed = encoder.finish().unwrap();
|
|
||||||
|
|
||||||
file.seek(SeekFrom::Start(append_offset)).unwrap();
|
|
||||||
file.write_all(&compressed).unwrap();
|
|
||||||
|
|
||||||
// The L2 entry encodes the compressed size in units of
|
|
||||||
// 512 byte sectors. The reader decodes the sector count
|
|
||||||
// back and computes: nsectors * 512 - (addr & 511).
|
|
||||||
// Because addr is 512 aligned, this yields nsectors * 512
|
|
||||||
// which rounds up to the next sector boundary. The file
|
|
||||||
// must contain enough bytes for that rounded up pread.
|
|
||||||
let padded_len = (compressed.len() + 511) & !511;
|
|
||||||
if padded_len > compressed.len() {
|
|
||||||
let padding = vec![0u8; padded_len - compressed.len()];
|
|
||||||
file.write_all(&padding).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_entry =
|
|
||||||
make_compressed_l2_entry(append_offset, compressed.len(), cluster_bits);
|
|
||||||
file.seek(SeekFrom::Start(l2_entry_offset)).unwrap();
|
|
||||||
file.write_u64::<BigEndian>(new_entry).unwrap();
|
|
||||||
|
|
||||||
append_offset += padded_len as u64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pread_alloc() {
|
|
||||||
let temp = TempFile::new().unwrap();
|
|
||||||
let file = temp.as_file();
|
|
||||||
let data: Vec<u8> = (0..=255).cycle().take(4096).collect();
|
|
||||||
file.write_all_at(&data, 0).unwrap();
|
|
||||||
|
|
||||||
let buf = pread_alloc(file.as_raw_fd(), 0, 4096).unwrap();
|
|
||||||
assert_eq!(buf, data);
|
|
||||||
|
|
||||||
let buf = pread_alloc(file.as_raw_fd(), 100, 200).unwrap();
|
|
||||||
assert_eq!(buf, &data[100..300]);
|
|
||||||
|
|
||||||
pread_alloc(file.as_raw_fd(), 4000, 200).unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_decompress_cluster() {
|
|
||||||
let cluster_size = 65536;
|
|
||||||
let original: Vec<u8> = (0..=255).cycle().take(cluster_size).collect();
|
|
||||||
|
|
||||||
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
|
|
||||||
encoder.write_all(&original).unwrap();
|
|
||||||
let compressed = encoder.finish().unwrap();
|
|
||||||
|
|
||||||
let result = decompress_cluster(&compressed, cluster_size, &ZlibDecoder {}).unwrap();
|
|
||||||
assert_eq!(result, original);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_decompress_cluster_corrupt_input() {
|
|
||||||
let corrupt = vec![0xffu8; 64];
|
|
||||||
let err = decompress_cluster(&corrupt, 65536, &ZlibDecoder {}).unwrap_err();
|
|
||||||
assert_eq!(err.raw_os_error(), Some(libc::EIO));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{fmt, io};
|
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
|
|
||||||
use crate::disk_file;
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
|
||||||
use crate::qcow::backing::shared_backing_from;
|
|
||||||
use crate::qcow::metadata::{BackingRead, QcowMetadata};
|
|
||||||
use crate::qcow::qcow_raw_file::QcowRawFile;
|
|
||||||
use crate::qcow::{MAX_NESTING_DEPTH, RawFile, parse_qcow};
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
use crate::qcow_async::QcowAsync;
|
|
||||||
use crate::qcow_sync::QcowSync;
|
|
||||||
|
|
||||||
/// Unified DiskFile wrapper for QCOW2 disk images.
|
|
||||||
///
|
|
||||||
/// Holds the in memory QCOW2 metadata, the data file, and an optional
|
|
||||||
/// backing file. The metadata is wrapped in an `Arc` because
|
|
||||||
/// [`QcowSync`] and [`QcowAsync`] I/O workers receive a clone when
|
|
||||||
/// they are created via [`create_async_io`](DiskFile::create_async_io).
|
|
||||||
/// The backing file is likewise shared with workers through an `Arc`.
|
|
||||||
///
|
|
||||||
/// The `sparse` flag controls whether the image advertises discard
|
|
||||||
/// support to the guest. The `use_io_uring` flag selects between the
|
|
||||||
/// [`QcowSync`] and [`QcowAsync`] I/O backends. Both are recorded at
|
|
||||||
/// construction time and propagated through [`try_clone`](DiskFile::try_clone).
|
|
||||||
pub struct QcowDisk {
|
|
||||||
metadata: Arc<QcowMetadata>,
|
|
||||||
backing_file: Option<Arc<dyn BackingRead>>,
|
|
||||||
sparse: bool,
|
|
||||||
data_raw_file: QcowRawFile,
|
|
||||||
use_io_uring: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for QcowDisk {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("QcowDisk")
|
|
||||||
.field("sparse", &self.sparse)
|
|
||||||
.field("has_backing", &self.backing_file.is_some())
|
|
||||||
.field("use_io_uring", &self.use_io_uring)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QcowDisk {
|
|
||||||
pub fn new(
|
|
||||||
file: File,
|
|
||||||
direct_io: bool,
|
|
||||||
backing_files: bool,
|
|
||||||
sparse: bool,
|
|
||||||
use_io_uring: bool,
|
|
||||||
) -> BlockResult<Self> {
|
|
||||||
#[cfg(not(feature = "io_uring"))]
|
|
||||||
if use_io_uring {
|
|
||||||
return Err(BlockError::new(
|
|
||||||
BlockErrorKind::UnsupportedFeature,
|
|
||||||
DiskFileError::NewAsyncIo(io::Error::other(
|
|
||||||
"io_uring requested but feature is not enabled",
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 };
|
|
||||||
let raw_file = RawFile::new(file, direct_io);
|
|
||||||
let (inner, backing_file, sparse) = parse_qcow(raw_file, max_nesting_depth, sparse)
|
|
||||||
.map_err(|e| {
|
|
||||||
let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) {
|
|
||||||
e.with_kind(BlockErrorKind::UnsupportedFeature)
|
|
||||||
} else {
|
|
||||||
e
|
|
||||||
};
|
|
||||||
e.with_op(ErrorOp::Open)
|
|
||||||
})?;
|
|
||||||
let data_raw_file = inner.raw_file.clone();
|
|
||||||
Ok(QcowDisk {
|
|
||||||
metadata: Arc::new(QcowMetadata::new(inner)),
|
|
||||||
backing_file: backing_file.map(shared_backing_from).transpose()?,
|
|
||||||
sparse,
|
|
||||||
data_raw_file,
|
|
||||||
use_io_uring,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for QcowDisk {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.metadata.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskSize for QcowDisk {
|
|
||||||
fn logical_size(&self) -> BlockResult<u64> {
|
|
||||||
Ok(self.metadata.virtual_size())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::PhysicalSize for QcowDisk {
|
|
||||||
fn physical_size(&self) -> BlockResult<u64> {
|
|
||||||
Ok(self.data_raw_file.physical_size()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFd for QcowDisk {
|
|
||||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
|
||||||
BorrowedDiskFd::new(self.data_raw_file.as_raw_fd())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Geometry for QcowDisk {}
|
|
||||||
|
|
||||||
impl disk_file::SparseCapable for QcowDisk {
|
|
||||||
fn supports_sparse_operations(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_zero_flag(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Resizable for QcowDisk {
|
|
||||||
fn resize(&mut self, size: u64) -> BlockResult<()> {
|
|
||||||
if self.backing_file.is_some() {
|
|
||||||
return Err(BlockError::new(
|
|
||||||
BlockErrorKind::UnsupportedFeature,
|
|
||||||
DiskFileError::ResizeError(io::Error::other(
|
|
||||||
"resize not supported with backing files",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_op(ErrorOp::Resize));
|
|
||||||
}
|
|
||||||
self.metadata.resize(size).map_err(|e| {
|
|
||||||
BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e))
|
|
||||||
.with_op(ErrorOp::Resize)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFile for QcowDisk {}
|
|
||||||
|
|
||||||
impl disk_file::AsyncDiskFile for QcowDisk {
|
|
||||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
|
||||||
Ok(Box::new(QcowDisk {
|
|
||||||
metadata: Arc::clone(&self.metadata),
|
|
||||||
backing_file: self.backing_file.as_ref().map(Arc::clone),
|
|
||||||
sparse: self.sparse,
|
|
||||||
data_raw_file: self.data_raw_file.clone(),
|
|
||||||
use_io_uring: self.use_io_uring,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
|
||||||
if self.use_io_uring {
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
{
|
|
||||||
return Ok(Box::new(
|
|
||||||
QcowAsync::new(
|
|
||||||
Arc::clone(&self.metadata),
|
|
||||||
self.data_raw_file.clone(),
|
|
||||||
self.backing_file.as_ref().map(Arc::clone),
|
|
||||||
self.sparse,
|
|
||||||
ring_depth,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e))
|
|
||||||
})?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "io_uring"))]
|
|
||||||
unreachable!("use_io_uring is set but io_uring feature is not enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = ring_depth;
|
|
||||||
Ok(Box::new(QcowSync::new(
|
|
||||||
Arc::clone(&self.metadata),
|
|
||||||
self.data_raw_file.clone(),
|
|
||||||
self.backing_file.as_ref().map(Arc::clone),
|
|
||||||
self.sparse,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::async_io::AsyncIo;
|
|
||||||
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize};
|
|
||||||
use crate::qcow::{QcowFile, RawFile};
|
|
||||||
|
|
||||||
const TEST_SIZE: u64 = 0x5566_7788;
|
|
||||||
|
|
||||||
fn make_qcow_file() -> File {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
{
|
|
||||||
let raw = RawFile::new(temp_file.as_file().try_clone().unwrap(), false);
|
|
||||||
QcowFile::new(raw, 3, TEST_SIZE, true).unwrap();
|
|
||||||
}
|
|
||||||
temp_file.into_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_sync_returns_correct_size() {
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
|
||||||
assert_eq!(disk.logical_size().unwrap(), TEST_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) {
|
|
||||||
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
|
|
||||||
assert_eq!(io.batch_requests_enabled(), expect_batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_async_io(disk: &QcowDisk, expect_batch: bool) {
|
|
||||||
assert_async_io_from_dyn(disk, expect_batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_backend_disables_batch_requests() {
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
|
||||||
assert_async_io(&disk, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn io_uring_backend_enables_batch_requests() {
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, true).unwrap();
|
|
||||||
assert_async_io(&disk, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_sync_dispatch() {
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
|
||||||
let cloned = disk.try_clone().unwrap();
|
|
||||||
assert_async_io_from_dyn(cloned.as_ref(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_io_uring_dispatch() {
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, true).unwrap();
|
|
||||||
let cloned = disk.try_clone().unwrap();
|
|
||||||
assert_async_io_from_dyn(cloned.as_ref(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn physical_size_less_than_logical() {
|
|
||||||
// make_qcow_file() writes no guest data, so the file on disk
|
|
||||||
// only contains QCOW2 headers and metadata tables.
|
|
||||||
let file = make_qcow_file();
|
|
||||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
|
||||||
assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,43 +2,87 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||||
|
|
||||||
use std::io::Error;
|
use std::fs::File;
|
||||||
|
use std::io::{Error, Seek, SeekFrom};
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use io_uring::{IoUring, opcode, types};
|
use io_uring::{IoUring, opcode, types};
|
||||||
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
|
use log::warn;
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
use crate::async_io::{
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
use crate::{BatchRequest, RequestType, SECTOR_SIZE};
|
};
|
||||||
|
use crate::{BatchRequest, DiskTopology, RequestType};
|
||||||
|
|
||||||
|
pub struct RawFileDisk {
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawFileDisk {
|
||||||
|
pub fn new(file: File) -> Self {
|
||||||
|
RawFileDisk { file }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskFile for RawFileDisk {
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.seek(SeekFrom::End(0))
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.metadata()
|
||||||
|
.map(|m| m.len())
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
RawFileAsync::new(self.file.as_raw_fd(), ring_depth)
|
||||||
|
.map_err(DiskFileError::NewAsyncIo)?,
|
||||||
|
) as Box<dyn AsyncIo>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn topology(&mut self) -> DiskTopology {
|
||||||
|
if let Ok(topology) = DiskTopology::probe(&self.file) {
|
||||||
|
topology
|
||||||
|
} else {
|
||||||
|
warn!("Unable to get device topology. Using default topology");
|
||||||
|
DiskTopology::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&mut self, size: u64) -> DiskFileResult<()> {
|
||||||
|
self.file.set_len(size).map_err(DiskFileError::ResizeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
|
BorrowedDiskFd::new(self.file.as_raw_fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RawFileAsync {
|
pub struct RawFileAsync {
|
||||||
fd: RawFd,
|
fd: RawFd,
|
||||||
io_uring: IoUring,
|
io_uring: IoUring,
|
||||||
eventfd: EventFd,
|
eventfd: EventFd,
|
||||||
alignment: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawFileAsync {
|
impl RawFileAsync {
|
||||||
pub fn new(fd: RawFd, ring_depth: u32) -> BlockResult<Self> {
|
pub fn new(fd: RawFd, ring_depth: u32) -> std::io::Result<Self> {
|
||||||
let io_uring =
|
let io_uring = IoUring::new(ring_depth)?;
|
||||||
IoUring::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
let eventfd = EventFd::new(libc::EFD_NONBLOCK)?;
|
||||||
let eventfd =
|
|
||||||
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
||||||
|
|
||||||
// Register the io_uring eventfd that will notify when something in
|
// Register the io_uring eventfd that will notify when something in
|
||||||
// the completion queue is ready.
|
// the completion queue is ready.
|
||||||
io_uring
|
io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?;
|
||||||
.submitter()
|
|
||||||
.register_eventfd(eventfd.as_raw_fd())
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
||||||
|
|
||||||
Ok(RawFileAsync {
|
Ok(RawFileAsync {
|
||||||
fd,
|
fd,
|
||||||
io_uring,
|
io_uring,
|
||||||
eventfd,
|
eventfd,
|
||||||
alignment: SECTOR_SIZE,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,10 +92,6 @@ impl AsyncIo for RawFileAsync {
|
|||||||
&self.eventfd
|
&self.eventfd
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alignment(&self) -> u64 {
|
|
||||||
self.alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_vectored(
|
fn read_vectored(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: libc::off_t,
|
offset: libc::off_t,
|
||||||
@@ -69,9 +109,7 @@ impl AsyncIo for RawFileAsync {
|
|||||||
.build()
|
.build()
|
||||||
.user_data(user_data),
|
.user_data(user_data),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|_| AsyncIoError::ReadVectored(Error::other("Submission queue is full")))?;
|
||||||
AsyncIoError::ReadVectored(Error::other(format!("Submission queue is full: {e:?}")))
|
|
||||||
})?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the submission queue and submit new operations to the
|
// Update the submission queue and submit new operations to the
|
||||||
@@ -99,11 +137,7 @@ impl AsyncIo for RawFileAsync {
|
|||||||
.build()
|
.build()
|
||||||
.user_data(user_data),
|
.user_data(user_data),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|_| AsyncIoError::WriteVectored(Error::other("Submission queue is full")))?;
|
||||||
AsyncIoError::WriteVectored(Error::other(format!(
|
|
||||||
"Submission queue is full: {e:?}"
|
|
||||||
)))
|
|
||||||
})?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the submission queue and submit new operations to the
|
// Update the submission queue and submit new operations to the
|
||||||
@@ -125,9 +159,7 @@ impl AsyncIo for RawFileAsync {
|
|||||||
.build()
|
.build()
|
||||||
.user_data(user_data),
|
.user_data(user_data),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|_| AsyncIoError::Fsync(Error::other("Submission queue is full")))?;
|
||||||
AsyncIoError::Fsync(Error::other(format!("Submission queue is full: {e:?}")))
|
|
||||||
})?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the submission queue and submit new operations to the
|
// Update the submission queue and submit new operations to the
|
||||||
@@ -161,14 +193,6 @@ impl AsyncIo for RawFileAsync {
|
|||||||
let (submitter, mut sq, _) = self.io_uring.split();
|
let (submitter, mut sq, _) = self.io_uring.split();
|
||||||
let mut submitted = false;
|
let mut submitted = false;
|
||||||
|
|
||||||
// Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially
|
|
||||||
// successful push.
|
|
||||||
if batch_request.len() > sq.capacity() - sq.len() {
|
|
||||||
return Err(AsyncIoError::SubmitBatchRequests(Error::other(
|
|
||||||
"io_uring submission queue is full",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
for req in batch_request {
|
for req in batch_request {
|
||||||
match req.request_type {
|
match req.request_type {
|
||||||
RequestType::In => {
|
RequestType::In => {
|
||||||
@@ -185,10 +209,8 @@ impl AsyncIo for RawFileAsync {
|
|||||||
.build()
|
.build()
|
||||||
.user_data(req.user_data),
|
.user_data(req.user_data),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|_| {
|
||||||
AsyncIoError::ReadVectored(Error::other(format!(
|
AsyncIoError::ReadVectored(Error::other("Submission queue is full"))
|
||||||
"Submission queue is full: {e:?}"
|
|
||||||
)))
|
|
||||||
})?;
|
})?;
|
||||||
};
|
};
|
||||||
submitted = true;
|
submitted = true;
|
||||||
@@ -207,10 +229,8 @@ impl AsyncIo for RawFileAsync {
|
|||||||
.build()
|
.build()
|
||||||
.user_data(req.user_data),
|
.user_data(req.user_data),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|_| {
|
||||||
AsyncIoError::WriteVectored(Error::other(format!(
|
AsyncIoError::WriteVectored(Error::other("Submission queue is full"))
|
||||||
"Submission queue is full: {e:?}"
|
|
||||||
)))
|
|
||||||
})?;
|
})?;
|
||||||
};
|
};
|
||||||
submitted = true;
|
submitted = true;
|
||||||
@@ -233,54 +253,4 @@ impl AsyncIo for RawFileAsync {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
let (submitter, mut sq, _) = self.io_uring.split();
|
|
||||||
|
|
||||||
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: The file descriptor is known to be valid.
|
|
||||||
unsafe {
|
|
||||||
sq.push(
|
|
||||||
&opcode::Fallocate::new(types::Fd(self.fd), length)
|
|
||||||
.offset(offset)
|
|
||||||
.mode(mode)
|
|
||||||
.build()
|
|
||||||
.user_data(user_data),
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
AsyncIoError::PunchHole(Error::other(format!("Submission queue is full: {e:?}")))
|
|
||||||
})?;
|
|
||||||
};
|
|
||||||
|
|
||||||
sq.sync();
|
|
||||||
submitter.submit().map_err(AsyncIoError::PunchHole)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
let (submitter, mut sq, _) = self.io_uring.split();
|
|
||||||
|
|
||||||
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: The file descriptor is known to be valid.
|
|
||||||
unsafe {
|
|
||||||
sq.push(
|
|
||||||
&opcode::Fallocate::new(types::Fd(self.fd), length)
|
|
||||||
.offset(offset)
|
|
||||||
.mode(mode)
|
|
||||||
.build()
|
|
||||||
.user_data(user_data),
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
AsyncIoError::WriteZeroes(Error::other(format!("Submission queue is full: {e:?}")))
|
|
||||||
})?;
|
|
||||||
};
|
|
||||||
|
|
||||||
sq.sync();
|
|
||||||
submitter.submit().map_err(AsyncIoError::WriteZeroes)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,39 +5,76 @@
|
|||||||
// Copyright © 2023 Crusoe Energy Systems LLC
|
// Copyright © 2023 Crusoe Energy Systems LLC
|
||||||
//
|
//
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::fs::File;
|
||||||
|
use std::io::{Seek, SeekFrom};
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
|
use log::warn;
|
||||||
use vmm_sys_util::aio;
|
use vmm_sys_util::aio;
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::SECTOR_SIZE;
|
use crate::DiskTopology;
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
use crate::async_io::{
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RawFileDiskAio {
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawFileDiskAio {
|
||||||
|
pub fn new(file: File) -> Self {
|
||||||
|
RawFileDiskAio { file }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskFile for RawFileDiskAio {
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.seek(SeekFrom::End(0))
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.metadata()
|
||||||
|
.map(|m| m.len())
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
RawFileAsyncAio::new(self.file.as_raw_fd(), ring_depth)
|
||||||
|
.map_err(DiskFileError::NewAsyncIo)?,
|
||||||
|
) as Box<dyn AsyncIo>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn topology(&mut self) -> DiskTopology {
|
||||||
|
if let Ok(topology) = DiskTopology::probe(&self.file) {
|
||||||
|
topology
|
||||||
|
} else {
|
||||||
|
warn!("Unable to get device topology. Using default topology");
|
||||||
|
DiskTopology::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
|
BorrowedDiskFd::new(self.file.as_raw_fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RawFileAsyncAio {
|
pub struct RawFileAsyncAio {
|
||||||
fd: RawFd,
|
fd: RawFd,
|
||||||
ctx: aio::IoContext,
|
ctx: aio::IoContext,
|
||||||
eventfd: EventFd,
|
eventfd: EventFd,
|
||||||
alignment: u64,
|
|
||||||
completion_list: VecDeque<(u64, i32)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawFileAsyncAio {
|
impl RawFileAsyncAio {
|
||||||
pub fn new(fd: RawFd, queue_depth: u32) -> BlockResult<Self> {
|
pub fn new(fd: RawFd, queue_depth: u32) -> std::io::Result<Self> {
|
||||||
let eventfd =
|
let eventfd = EventFd::new(libc::EFD_NONBLOCK)?;
|
||||||
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
let ctx = aio::IoContext::new(queue_depth)?;
|
||||||
let ctx =
|
|
||||||
aio::IoContext::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
||||||
|
|
||||||
Ok(RawFileAsyncAio {
|
Ok(RawFileAsyncAio { fd, ctx, eventfd })
|
||||||
fd,
|
|
||||||
ctx,
|
|
||||||
eventfd,
|
|
||||||
alignment: SECTOR_SIZE,
|
|
||||||
completion_list: VecDeque::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +83,6 @@ impl AsyncIo for RawFileAsyncAio {
|
|||||||
&self.eventfd
|
&self.eventfd
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alignment(&self) -> u64 {
|
|
||||||
self.alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_vectored(
|
fn read_vectored(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: libc::off_t,
|
offset: libc::off_t,
|
||||||
@@ -120,99 +153,12 @@ impl AsyncIo for RawFileAsyncAio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||||
if self.completion_list.is_empty() {
|
let mut events: [aio::IoEvent; 1] = [aio::IoEvent::default()];
|
||||||
// Drain pending AIO completions batched into the same queue.
|
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
|
||||||
let mut events = [aio::IoEvent::default(); 32];
|
if rc == 0 {
|
||||||
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
|
None
|
||||||
for event in &events[..rc] {
|
} else {
|
||||||
self.completion_list
|
Some((events[0].data, events[0].res as i32))
|
||||||
.push_back((event.data, event.res as i32));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.completion_list.pop_front()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
// Linux AIO has no IOCB command for fallocate, so perform the operation
|
|
||||||
// synchronously and signal completion via the completion list, matching
|
|
||||||
// the pattern used by the sync backend (RawFileSync).
|
|
||||||
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid arguments
|
|
||||||
let result = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
self.fd as libc::c_int,
|
|
||||||
mode,
|
|
||||||
offset as libc::off_t,
|
|
||||||
length as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result < 0 {
|
|
||||||
return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error()));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.completion_list.push_back((user_data, result));
|
|
||||||
self.eventfd.write(1).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
// Linux AIO has no IOCB command for fallocate, so perform the operation
|
|
||||||
// synchronously and signal completion via the completion list, matching
|
|
||||||
// the pattern used by the sync backend (RawFileSync).
|
|
||||||
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid arguments
|
|
||||||
let result = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
self.fd as libc::c_int,
|
|
||||||
mode,
|
|
||||||
offset as libc::off_t,
|
|
||||||
length as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result < 0 {
|
|
||||||
return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error()));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.completion_list.push_back((user_data, result));
|
|
||||||
self.eventfd.write(1).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::raw_async_io_tests;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_punch_hole() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
|
||||||
raw_async_io_tests::test_punch_hole(&mut async_io, &mut file);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_zeroes() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
|
||||||
raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_punch_hole_multiple_operations() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
|
|
||||||
raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
//! Shared test helpers for [`AsyncIo`] backends.
|
|
||||||
//!
|
|
||||||
//! Each helper takes a `&mut dyn AsyncIo` together with the [`File`] handle
|
|
||||||
//! that backs the I/O object, so the same logic exercises every backend with
|
|
||||||
//! only the constructor differing.
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError};
|
|
||||||
|
|
||||||
/// Tests punching a hole in the middle of a 4 MB file and verifying data
|
|
||||||
/// integrity around the hole.
|
|
||||||
pub fn test_punch_hole(async_io: &mut dyn AsyncIo, file: &mut File) {
|
|
||||||
// Write 4MB of data
|
|
||||||
let data = vec![0xAA; 4 * 1024 * 1024];
|
|
||||||
file.write_all(&data).unwrap();
|
|
||||||
file.sync_all().unwrap();
|
|
||||||
|
|
||||||
// Punch hole in the middle (1MB at offset 1MB)
|
|
||||||
let offset = 1024 * 1024;
|
|
||||||
let length = 1024 * 1024;
|
|
||||||
async_io.punch_hole(offset, length, 1).unwrap();
|
|
||||||
|
|
||||||
// Check completion
|
|
||||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
||||||
assert_eq!(user_data, 1);
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
// Verify the hole reads as zeros
|
|
||||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
|
||||||
let mut read_buf = vec![0; length as usize];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0),
|
|
||||||
"Punched hole should read as zeros"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify data before hole is intact
|
|
||||||
file.seek(SeekFrom::Start(0)).unwrap();
|
|
||||||
let mut read_buf = vec![0; 1024];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0xAA),
|
|
||||||
"Data before hole should be intact"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify data after hole is intact
|
|
||||||
file.seek(SeekFrom::Start(offset + length)).unwrap();
|
|
||||||
let mut read_buf = vec![0; 1024];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0xAA),
|
|
||||||
"Data after hole should be intact"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests writing zeroes to a 512 KB region inside a 4 MB file and verifying
|
|
||||||
/// surrounding data is preserved. Gracefully skips when the filesystem does
|
|
||||||
/// not support `FALLOC_FL_ZERO_RANGE`.
|
|
||||||
pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) {
|
|
||||||
// Write 4MB of data
|
|
||||||
let data = vec![0xBB; 4 * 1024 * 1024];
|
|
||||||
file.write_all(&data).unwrap();
|
|
||||||
file.sync_all().unwrap();
|
|
||||||
|
|
||||||
// Write zeros in the middle (512KB at offset 2MB)
|
|
||||||
let offset = 2 * 1024 * 1024;
|
|
||||||
let length = 512 * 1024;
|
|
||||||
let write_zeroes_result = async_io.write_zeroes(offset, length, 2);
|
|
||||||
|
|
||||||
// FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs)
|
|
||||||
// If it fails with ENOTSUP, skip the test
|
|
||||||
if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result
|
|
||||||
&& (e.raw_os_error() == Some(libc::EOPNOTSUPP) || e.raw_os_error() == Some(libc::ENOTSUP))
|
|
||||||
{
|
|
||||||
eprintln!("Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
write_zeroes_result.unwrap();
|
|
||||||
|
|
||||||
// Check completion
|
|
||||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
||||||
assert_eq!(user_data, 2);
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
// Verify the zeroed region reads as zeros
|
|
||||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
|
||||||
let mut read_buf = vec![0; length as usize];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0),
|
|
||||||
"Zeroed region should read as zeros"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify data before zeroed region is intact
|
|
||||||
file.seek(SeekFrom::Start(offset - 1024)).unwrap();
|
|
||||||
let mut read_buf = vec![0; 1024];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0xBB),
|
|
||||||
"Data before zeroed region should be intact"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify data after zeroed region is intact
|
|
||||||
file.seek(SeekFrom::Start(offset + length)).unwrap();
|
|
||||||
let mut read_buf = vec![0; 1024];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(
|
|
||||||
read_buf.iter().all(|&b| b == 0xBB),
|
|
||||||
"Data after zeroed region should be intact"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests punching multiple holes in an 8 MB file and verifying each hole
|
|
||||||
/// independently reads as zeroes.
|
|
||||||
pub fn test_punch_hole_multiple_operations(async_io: &mut dyn AsyncIo, file: &mut File) {
|
|
||||||
// Write 8MB of data
|
|
||||||
let data = vec![0xCC; 8 * 1024 * 1024];
|
|
||||||
file.write_all(&data).unwrap();
|
|
||||||
file.sync_all().unwrap();
|
|
||||||
|
|
||||||
// Punch multiple holes
|
|
||||||
async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap();
|
|
||||||
async_io
|
|
||||||
.punch_hole(3 * 1024 * 1024, 512 * 1024, 11)
|
|
||||||
.unwrap();
|
|
||||||
async_io
|
|
||||||
.punch_hole(5 * 1024 * 1024, 512 * 1024, 12)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Check all completions
|
|
||||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
||||||
assert_eq!(user_data, 10);
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
||||||
assert_eq!(user_data, 11);
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
||||||
assert_eq!(user_data, 12);
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
// Verify all holes read as zeros
|
|
||||||
file.seek(SeekFrom::Start(1024 * 1024)).unwrap();
|
|
||||||
let mut read_buf = vec![0; 512 * 1024];
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(read_buf.iter().all(|&b| b == 0));
|
|
||||||
|
|
||||||
file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap();
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(read_buf.iter().all(|&b| b == 0));
|
|
||||||
|
|
||||||
file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap();
|
|
||||||
file.read_exact(&mut read_buf).unwrap();
|
|
||||||
assert!(read_buf.iter().all(|&b| b == 0));
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
|
||||||
use std::os::unix::fs::FileTypeExt;
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
|
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
use crate::raw_async::RawFileAsync;
|
|
||||||
use crate::raw_async_aio::RawFileAsyncAio;
|
|
||||||
use crate::raw_sync::RawFileSync;
|
|
||||||
use crate::{DiskTopology, disk_file, probe_sparse_support, query_device_size};
|
|
||||||
|
|
||||||
/// Selects which async I/O backend a `RawDisk` uses.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum RawBackend {
|
|
||||||
/// Blocking I/O where the caller waits for completion.
|
|
||||||
Sync,
|
|
||||||
/// Modern asynchronous I/O using shared submission and completion
|
|
||||||
/// rings for lower overhead operation dispatch and completion handling.
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
IoUring,
|
|
||||||
/// Legacy asynchronous I/O where requests are handed to the kernel
|
|
||||||
/// and completions are collected later.
|
|
||||||
Aio,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unified DiskFile wrapper for raw disk images.
|
|
||||||
///
|
|
||||||
/// Owns the underlying file and delegates async I/O creation to the
|
|
||||||
/// backend selected at construction time via [`RawBackend`].
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RawDisk {
|
|
||||||
file: File,
|
|
||||||
backend: RawBackend,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawDisk {
|
|
||||||
pub fn new(file: File, backend: RawBackend) -> Self {
|
|
||||||
Self { file, backend }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskSize for RawDisk {
|
|
||||||
fn logical_size(&self) -> BlockResult<u64> {
|
|
||||||
query_device_size(&self.file)
|
|
||||||
.map(|(logical_size, _)| logical_size)
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::PhysicalSize for RawDisk {
|
|
||||||
fn physical_size(&self) -> BlockResult<u64> {
|
|
||||||
query_device_size(&self.file)
|
|
||||||
.map(|(_, physical_size)| physical_size)
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFd for RawDisk {
|
|
||||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
|
||||||
BorrowedDiskFd::new(self.file.as_raw_fd())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Geometry for RawDisk {
|
|
||||||
fn topology(&self) -> DiskTopology {
|
|
||||||
DiskTopology::probe(&self.file).unwrap_or_else(|_| {
|
|
||||||
warn!("Unable to get device topology. Using default topology");
|
|
||||||
DiskTopology::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::SparseCapable for RawDisk {
|
|
||||||
fn supports_sparse_operations(&self) -> bool {
|
|
||||||
probe_sparse_support(&self.file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Resizable for RawDisk {
|
|
||||||
fn resize(&mut self, size: u64) -> BlockResult<()> {
|
|
||||||
let fd_metadata = self
|
|
||||||
.file
|
|
||||||
.metadata()
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?;
|
|
||||||
|
|
||||||
if fd_metadata.file_type().is_block_device() {
|
|
||||||
// Block devices cannot be resized via ftruncate; they are resized
|
|
||||||
// externally (LVM, losetup, etc.). Verify the size matches.
|
|
||||||
let (actual_size, _) = query_device_size(&self.file)
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?;
|
|
||||||
if actual_size != size {
|
|
||||||
return Err(BlockError::new(
|
|
||||||
BlockErrorKind::Io,
|
|
||||||
DiskFileError::ResizeError(io::Error::other(format!(
|
|
||||||
"Block device size {actual_size} does not match requested size {size}"
|
|
||||||
))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
self.file
|
|
||||||
.set_len(size)
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFile for RawDisk {}
|
|
||||||
|
|
||||||
impl disk_file::AsyncDiskFile for RawDisk {
|
|
||||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
|
||||||
let file = self
|
|
||||||
.file
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?;
|
|
||||||
Ok(Box::new(RawDisk {
|
|
||||||
file,
|
|
||||||
backend: self.backend,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
|
||||||
match self.backend {
|
|
||||||
RawBackend::Sync => Ok(Box::new(RawFileSync::new(self.file.as_raw_fd()))),
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
RawBackend::IoUring => Ok(Box::new(RawFileAsync::new(
|
|
||||||
self.file.as_raw_fd(),
|
|
||||||
ring_depth,
|
|
||||||
)?)),
|
|
||||||
RawBackend::Aio => Ok(Box::new(RawFileAsyncAio::new(
|
|
||||||
self.file.as_raw_fd(),
|
|
||||||
ring_depth,
|
|
||||||
)?)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::fs::File;
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::async_io::AsyncIo;
|
|
||||||
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable};
|
|
||||||
|
|
||||||
const TEST_SIZE: u64 = 0x1122_3344;
|
|
||||||
|
|
||||||
fn make_raw_file() -> File {
|
|
||||||
let file: File = TempFile::new().unwrap().into_file();
|
|
||||||
file.set_len(TEST_SIZE).unwrap();
|
|
||||||
file
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_sync_returns_correct_size() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Sync);
|
|
||||||
assert_eq!(disk.logical_size().unwrap(), TEST_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_backend: RawBackend) {
|
|
||||||
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
if #[cfg(feature = "io_uring")] {
|
|
||||||
let expected_batch_requests = expect_backend == RawBackend::IoUring;
|
|
||||||
} else {
|
|
||||||
let _ = expect_backend;
|
|
||||||
let expected_batch_requests = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(io.batch_requests_enabled(), expected_batch_requests);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_sync_backend(disk: &RawDisk) {
|
|
||||||
assert_eq!(disk.backend, RawBackend::Sync);
|
|
||||||
assert_async_io_from_dyn(disk, RawBackend::Sync);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_aio_backend(disk: &RawDisk) {
|
|
||||||
assert_eq!(disk.backend, RawBackend::Aio);
|
|
||||||
assert_async_io_from_dyn(disk, RawBackend::Aio);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
fn assert_io_uring_backend(disk: &RawDisk) {
|
|
||||||
assert_eq!(disk.backend, RawBackend::IoUring);
|
|
||||||
assert_async_io_from_dyn(disk, RawBackend::IoUring);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_backend_disables_batch_requests() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Sync);
|
|
||||||
assert_sync_backend(&disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aio_backend_disables_batch_requests() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Aio);
|
|
||||||
assert_aio_backend(&disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn io_uring_backend_enables_batch_requests() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::IoUring);
|
|
||||||
assert_io_uring_backend(&disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_try_clone(disk: &RawDisk, expect_backend: RawBackend) {
|
|
||||||
let cloned = disk.try_clone().unwrap();
|
|
||||||
assert_async_io_from_dyn(cloned.as_ref(), expect_backend);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_sync_backend() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Sync);
|
|
||||||
assert_try_clone(&disk, RawBackend::Sync);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_aio_backend() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Aio);
|
|
||||||
assert_try_clone(&disk, RawBackend::Aio);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "io_uring")]
|
|
||||||
#[test]
|
|
||||||
fn try_clone_preserves_io_uring_backend() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::IoUring);
|
|
||||||
assert_try_clone(&disk, RawBackend::IoUring);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resize_changes_file_size() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let mut disk = RawDisk::new(file, RawBackend::Aio);
|
|
||||||
let new_size = TEST_SIZE * 2;
|
|
||||||
disk.resize(new_size).unwrap();
|
|
||||||
assert_eq!(disk.logical_size().unwrap(), new_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn physical_size_reports_allocated_blocks() {
|
|
||||||
let file = make_raw_file();
|
|
||||||
let disk = RawDisk::new(file, RawBackend::Aio);
|
|
||||||
// Sparse file: physical size is less than logical size.
|
|
||||||
assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,19 +3,64 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::os::unix::io::RawFd;
|
use std::fs::File;
|
||||||
|
use std::io::{Seek, SeekFrom};
|
||||||
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
|
||||||
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
|
use log::warn;
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::SECTOR_SIZE;
|
use crate::DiskTopology;
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
use crate::async_io::{
|
||||||
|
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RawFileDiskSync {
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawFileDiskSync {
|
||||||
|
pub fn new(file: File) -> Self {
|
||||||
|
RawFileDiskSync { file }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskFile for RawFileDiskSync {
|
||||||
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.seek(SeekFrom::End(0))
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
|
self.file
|
||||||
|
.metadata()
|
||||||
|
.map(|m| m.len())
|
||||||
|
.map_err(DiskFileError::Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
|
Ok(Box::new(RawFileSync::new(self.file.as_raw_fd())) as Box<dyn AsyncIo>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn topology(&mut self) -> DiskTopology {
|
||||||
|
if let Ok(topology) = DiskTopology::probe(&self.file) {
|
||||||
|
topology
|
||||||
|
} else {
|
||||||
|
warn!("Unable to get device topology. Using default topology");
|
||||||
|
DiskTopology::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
|
BorrowedDiskFd::new(self.file.as_raw_fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RawFileSync {
|
pub struct RawFileSync {
|
||||||
fd: RawFd,
|
fd: RawFd,
|
||||||
eventfd: EventFd,
|
eventfd: EventFd,
|
||||||
completion_list: VecDeque<(u64, i32)>,
|
completion_list: VecDeque<(u64, i32)>,
|
||||||
alignment: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawFileSync {
|
impl RawFileSync {
|
||||||
@@ -24,7 +69,6 @@ impl RawFileSync {
|
|||||||
fd,
|
fd,
|
||||||
eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"),
|
eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"),
|
||||||
completion_list: VecDeque::new(),
|
completion_list: VecDeque::new(),
|
||||||
alignment: SECTOR_SIZE,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,10 +78,6 @@ impl AsyncIo for RawFileSync {
|
|||||||
&self.eventfd
|
&self.eventfd
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alignment(&self) -> u64 {
|
|
||||||
self.alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_vectored(
|
fn read_vectored(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: libc::off_t,
|
offset: libc::off_t,
|
||||||
@@ -106,82 +146,4 @@ impl AsyncIo for RawFileSync {
|
|||||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||||
self.completion_list.pop_front()
|
self.completion_list.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid arguments
|
|
||||||
let result = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
self.fd as libc::c_int,
|
|
||||||
mode,
|
|
||||||
offset as libc::off_t,
|
|
||||||
length as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result < 0 {
|
|
||||||
return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error()));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.completion_list.push_back((user_data, result));
|
|
||||||
self.eventfd.write(1).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
|
|
||||||
|
|
||||||
// SAFETY: FFI call with valid arguments
|
|
||||||
let result = unsafe {
|
|
||||||
libc::fallocate(
|
|
||||||
self.fd as libc::c_int,
|
|
||||||
mode,
|
|
||||||
offset as libc::off_t,
|
|
||||||
length as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result < 0 {
|
|
||||||
return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error()));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.completion_list.push_back((user_data, result));
|
|
||||||
self.eventfd.write(1).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod unit_tests {
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
use vmm_sys_util::tempfile::TempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::raw_async_io_tests;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_punch_hole() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileSync::new(file.as_raw_fd());
|
|
||||||
raw_async_io_tests::test_punch_hole(&mut async_io, &mut file);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_zeroes() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileSync::new(file.as_raw_fd());
|
|
||||||
raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_punch_hole_multiple_operations() {
|
|
||||||
let temp_file = TempFile::new().unwrap();
|
|
||||||
let mut file = temp_file.into_file();
|
|
||||||
let mut async_io = RawFileSync::new(file.as_raw_fd());
|
|
||||||
raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,573 +0,0 @@
|
|||||||
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE-BSD-3-Clause file.
|
|
||||||
//
|
|
||||||
// Copyright © 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
||||||
|
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
|
||||||
use std::mem;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use log::{error, warn};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use virtio_bindings::virtio_blk::{
|
|
||||||
VIRTIO_BLK_T_DISCARD, VIRTIO_BLK_T_WRITE_ZEROES, VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP,
|
|
||||||
virtio_blk_discard_write_zeroes,
|
|
||||||
};
|
|
||||||
use virtio_queue::DescriptorChain;
|
|
||||||
use vm_memory::bitmap::Bitmap;
|
|
||||||
use vm_memory::{
|
|
||||||
Address as _, Bytes as _, GuestAddress, GuestMemory as _, GuestMemoryError,
|
|
||||||
GuestMemoryLoadGuard,
|
|
||||||
};
|
|
||||||
use vm_virtio::{AccessPlatform, Translatable as _};
|
|
||||||
|
|
||||||
use crate::aligned_operation::AlignedOperation;
|
|
||||||
use crate::async_io::AsyncIo;
|
|
||||||
use crate::{Error, ExecuteError, request_type, sector};
|
|
||||||
|
|
||||||
const SECTOR_SHIFT: u8 = 9;
|
|
||||||
pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT;
|
|
||||||
|
|
||||||
/// Maximum number of segments per DISCARD or WRITE_ZEROES request.
|
|
||||||
pub const MAX_DISCARD_WRITE_ZEROES_SEG: u32 = 1;
|
|
||||||
/// Size and field offsets within `struct virtio_blk_discard_write_zeroes`.
|
|
||||||
const DISCARD_WZ_SEG_SIZE: u32 = mem::size_of::<virtio_blk_discard_write_zeroes>() as u32;
|
|
||||||
const DISCARD_WZ_MAX_PAYLOAD: u32 = DISCARD_WZ_SEG_SIZE * MAX_DISCARD_WRITE_ZEROES_SEG;
|
|
||||||
const DISCARD_WZ_SECTOR_OFFSET: u64 =
|
|
||||||
mem::offset_of!(virtio_blk_discard_write_zeroes, sector) as u64;
|
|
||||||
const DISCARD_WZ_NUM_SECTORS_OFFSET: u64 =
|
|
||||||
mem::offset_of!(virtio_blk_discard_write_zeroes, num_sectors) as u64;
|
|
||||||
const DISCARD_WZ_FLAGS_OFFSET: u64 = mem::offset_of!(virtio_blk_discard_write_zeroes, flags) as u64;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum RequestType {
|
|
||||||
In,
|
|
||||||
Out,
|
|
||||||
Flush,
|
|
||||||
GetDeviceId,
|
|
||||||
Discard,
|
|
||||||
WriteZeroes,
|
|
||||||
Unsupported(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32;
|
|
||||||
pub struct BatchRequest {
|
|
||||||
pub offset: libc::off_t,
|
|
||||||
pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
|
||||||
pub user_data: u64,
|
|
||||||
pub request_type: RequestType,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ExecuteAsync {
|
|
||||||
// `true` if the execution will complete asynchronously
|
|
||||||
pub async_complete: bool,
|
|
||||||
// request need to be batched for submission if any
|
|
||||||
pub batch_request: Option<BatchRequest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Request {
|
|
||||||
request_type: RequestType,
|
|
||||||
sector: u64,
|
|
||||||
data_descriptors: SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
|
||||||
status_addr: GuestAddress,
|
|
||||||
pub writeback: bool,
|
|
||||||
aligned_operations: SmallVec<[AlignedOperation; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
|
|
||||||
start: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
pub fn parse<B: Bitmap + 'static>(
|
|
||||||
desc_chain: &mut DescriptorChain<GuestMemoryLoadGuard<vm_memory::GuestMemoryMmap<B>>>,
|
|
||||||
access_platform: Option<&dyn AccessPlatform>,
|
|
||||||
) -> Result<Request, Error> {
|
|
||||||
let hdr_desc = desc_chain
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::DescriptorChainTooShort)
|
|
||||||
.inspect_err(|_| {
|
|
||||||
error!("Missing head descriptor");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// The head contains the request type which MUST be readable.
|
|
||||||
if hdr_desc.is_write_only() {
|
|
||||||
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
let hdr_desc_addr = hdr_desc
|
|
||||||
.addr()
|
|
||||||
.translate_gva(access_platform, hdr_desc.len() as usize)
|
|
||||||
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?;
|
|
||||||
|
|
||||||
let mut req = Request {
|
|
||||||
request_type: request_type(desc_chain.memory(), hdr_desc_addr)?,
|
|
||||||
sector: sector(desc_chain.memory(), hdr_desc_addr)?,
|
|
||||||
data_descriptors: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
|
|
||||||
status_addr: GuestAddress(0),
|
|
||||||
writeback: true,
|
|
||||||
aligned_operations: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
|
|
||||||
start: Instant::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_desc;
|
|
||||||
let mut desc = desc_chain
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::DescriptorChainTooShort)
|
|
||||||
.inspect_err(|_| {
|
|
||||||
error!("Only head descriptor present: request = {req:?}");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if desc.has_next() {
|
|
||||||
req.data_descriptors.reserve_exact(1);
|
|
||||||
while desc.has_next() {
|
|
||||||
if desc.is_write_only() && req.request_type == RequestType::Out {
|
|
||||||
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
|
||||||
}
|
|
||||||
if desc.is_write_only() && req.request_type == RequestType::Discard {
|
|
||||||
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
|
||||||
}
|
|
||||||
if desc.is_write_only() && req.request_type == RequestType::WriteZeroes {
|
|
||||||
return Err(Error::UnexpectedWriteOnlyDescriptor);
|
|
||||||
}
|
|
||||||
if !desc.is_write_only() && req.request_type == RequestType::In {
|
|
||||||
return Err(Error::UnexpectedReadOnlyDescriptor);
|
|
||||||
}
|
|
||||||
if !desc.is_write_only() && req.request_type == RequestType::GetDeviceId {
|
|
||||||
return Err(Error::UnexpectedReadOnlyDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.data_descriptors.push((
|
|
||||||
desc.addr()
|
|
||||||
.translate_gva(access_platform, desc.len() as usize)
|
|
||||||
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?,
|
|
||||||
desc.len(),
|
|
||||||
));
|
|
||||||
desc = desc_chain
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::DescriptorChainTooShort)
|
|
||||||
.inspect_err(|_| {
|
|
||||||
error!("DescriptorChain corrupted: request = {req:?}");
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
status_desc = desc;
|
|
||||||
} else {
|
|
||||||
status_desc = desc;
|
|
||||||
// Only flush requests are allowed to skip the data descriptor.
|
|
||||||
if req.request_type != RequestType::Flush {
|
|
||||||
error!("Need a data descriptor: request = {req:?}");
|
|
||||||
return Err(Error::DescriptorChainTooShort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The status MUST always be writable.
|
|
||||||
if !status_desc.is_write_only() {
|
|
||||||
return Err(Error::UnexpectedReadOnlyDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if status_desc.len() < 1 {
|
|
||||||
return Err(Error::DescriptorLengthTooSmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.status_addr = status_desc
|
|
||||||
.addr()
|
|
||||||
.translate_gva(access_platform, status_desc.len() as usize)
|
|
||||||
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?;
|
|
||||||
|
|
||||||
Ok(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute<T: Seek + Read + Write, B: Bitmap + 'static>(
|
|
||||||
&self,
|
|
||||||
disk: &mut T,
|
|
||||||
disk_nsectors: u64,
|
|
||||||
mem: &vm_memory::GuestMemoryMmap<B>,
|
|
||||||
serial: &[u8],
|
|
||||||
) -> Result<u32, ExecuteError> {
|
|
||||||
self.check_data_bounds(disk_nsectors)?;
|
|
||||||
|
|
||||||
disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT))
|
|
||||||
.map_err(ExecuteError::Seek)?;
|
|
||||||
let mut len = 0;
|
|
||||||
for (data_addr, data_len) in &self.data_descriptors {
|
|
||||||
match self.request_type {
|
|
||||||
RequestType::In => {
|
|
||||||
let mut buf = vec![0u8; *data_len as usize];
|
|
||||||
disk.read_exact(&mut buf).map_err(ExecuteError::ReadExact)?;
|
|
||||||
mem.read_exact_volatile_from(
|
|
||||||
*data_addr,
|
|
||||||
&mut buf.as_slice(),
|
|
||||||
*data_len as usize,
|
|
||||||
)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
len += data_len;
|
|
||||||
}
|
|
||||||
RequestType::Out => {
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
|
||||||
mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize)
|
|
||||||
.map_err(ExecuteError::Write)?;
|
|
||||||
disk.write_all(&buf).map_err(ExecuteError::WriteAll)?;
|
|
||||||
if !self.writeback {
|
|
||||||
disk.flush().map_err(ExecuteError::Flush)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RequestType::Flush => disk.flush().map_err(ExecuteError::Flush)?,
|
|
||||||
RequestType::GetDeviceId => {
|
|
||||||
if (*data_len as usize) < serial.len() {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
mem.write_slice(serial, *data_addr)
|
|
||||||
.map_err(ExecuteError::Write)?;
|
|
||||||
}
|
|
||||||
RequestType::Discard => {
|
|
||||||
return Err(ExecuteError::Unsupported(VIRTIO_BLK_T_DISCARD));
|
|
||||||
}
|
|
||||||
RequestType::WriteZeroes => {
|
|
||||||
return Err(ExecuteError::Unsupported(VIRTIO_BLK_T_WRITE_ZEROES));
|
|
||||||
}
|
|
||||||
RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(len)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_async<B: Bitmap + 'static>(
|
|
||||||
&mut self,
|
|
||||||
mem: &vm_memory::GuestMemoryMmap<B>,
|
|
||||||
disk_nsectors: u64,
|
|
||||||
disk_image: &mut dyn AsyncIo,
|
|
||||||
serial: &[u8],
|
|
||||||
disable_sector0_writes: bool,
|
|
||||||
user_data: u64,
|
|
||||||
) -> Result<ExecuteAsync, ExecuteError> {
|
|
||||||
let sector = self.sector;
|
|
||||||
let request_type = self.request_type;
|
|
||||||
let offset = (sector << SECTOR_SHIFT) as libc::off_t;
|
|
||||||
let alignment = disk_image.alignment();
|
|
||||||
|
|
||||||
self.check_data_bounds(disk_nsectors)?;
|
|
||||||
|
|
||||||
let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
|
|
||||||
SmallVec::with_capacity(self.data_descriptors.len());
|
|
||||||
for &(data_addr, data_len) in &self.data_descriptors {
|
|
||||||
let _: u32 = data_len; // compiler-checked documentation
|
|
||||||
const _: () = assert!(
|
|
||||||
core::mem::size_of::<u32>() <= core::mem::size_of::<usize>(),
|
|
||||||
"unsupported platform"
|
|
||||||
);
|
|
||||||
if data_len == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let data_len = data_len as usize;
|
|
||||||
|
|
||||||
let origin_ptr = mem
|
|
||||||
.get_slice(data_addr, data_len)
|
|
||||||
.map_err(ExecuteError::GetHostAddress)?;
|
|
||||||
assert!(origin_ptr.len() >= data_len);
|
|
||||||
let origin_ptr = origin_ptr.ptr_guard_mut();
|
|
||||||
|
|
||||||
// O_DIRECT requires buffer addresses to be aligned to the
|
|
||||||
// backend device's logical block size. In case it's not properly
|
|
||||||
// aligned, an intermediate buffer is created with the correct
|
|
||||||
// alignment, and a copy from/to the origin buffer is performed,
|
|
||||||
// depending on the type of operation.
|
|
||||||
let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(alignment) {
|
|
||||||
origin_ptr.as_ptr().cast()
|
|
||||||
} else {
|
|
||||||
let mut aligned_op = AlignedOperation::new(data_addr, data_len, alignment as usize)
|
|
||||||
.map_err(ExecuteError::TemporaryBufferAllocation)?;
|
|
||||||
|
|
||||||
// We need to perform the copy beforehand in case we're writing
|
|
||||||
// data out.
|
|
||||||
if request_type == RequestType::Out {
|
|
||||||
mem.read_slice(aligned_op.as_bytes_mut(), data_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let aligned_ptr = aligned_op.as_mut_ptr();
|
|
||||||
self.aligned_operations.push(aligned_op);
|
|
||||||
|
|
||||||
aligned_ptr.cast()
|
|
||||||
};
|
|
||||||
|
|
||||||
let iovec = libc::iovec {
|
|
||||||
iov_base,
|
|
||||||
iov_len: data_len as libc::size_t,
|
|
||||||
};
|
|
||||||
iovecs.push(iovec);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ret = ExecuteAsync {
|
|
||||||
async_complete: true,
|
|
||||||
batch_request: None,
|
|
||||||
};
|
|
||||||
// Queue operations expected to be submitted.
|
|
||||||
match request_type {
|
|
||||||
RequestType::In => {
|
|
||||||
for (data_addr, data_len) in &self.data_descriptors {
|
|
||||||
mem.get_slice(*data_addr, *data_len as usize)
|
|
||||||
.map_err(ExecuteError::GetHostAddress)?
|
|
||||||
.bitmap()
|
|
||||||
.mark_dirty(0, *data_len as usize);
|
|
||||||
}
|
|
||||||
if disk_image.batch_requests_enabled() {
|
|
||||||
ret.batch_request = Some(BatchRequest {
|
|
||||||
offset,
|
|
||||||
iovecs,
|
|
||||||
user_data,
|
|
||||||
request_type,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
disk_image
|
|
||||||
.read_vectored(offset, &iovecs, user_data)
|
|
||||||
.map_err(ExecuteError::AsyncRead)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RequestType::Out => {
|
|
||||||
if disk_image.batch_requests_enabled() {
|
|
||||||
ret.batch_request = Some(BatchRequest {
|
|
||||||
offset,
|
|
||||||
iovecs,
|
|
||||||
user_data,
|
|
||||||
request_type,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
disk_image
|
|
||||||
.write_vectored(offset, &iovecs, user_data)
|
|
||||||
.map_err(ExecuteError::AsyncWrite)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RequestType::Flush => {
|
|
||||||
disk_image
|
|
||||||
.fsync(Some(user_data))
|
|
||||||
.map_err(ExecuteError::AsyncFlush)?;
|
|
||||||
}
|
|
||||||
RequestType::GetDeviceId => {
|
|
||||||
let (data_addr, data_len) = if self.data_descriptors.len() == 1 {
|
|
||||||
(self.data_descriptors[0].0, self.data_descriptors[0].1)
|
|
||||||
} else {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::TooManyDescriptors));
|
|
||||||
};
|
|
||||||
if (data_len as usize) < serial.len() {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
mem.write_slice(serial, data_addr)
|
|
||||||
.map_err(ExecuteError::Write)?;
|
|
||||||
ret.async_complete = false;
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
RequestType::Discard => {
|
|
||||||
let (data_addr, data_len) = if self.data_descriptors.len() == 1 {
|
|
||||||
(self.data_descriptors[0].0, self.data_descriptors[0].1)
|
|
||||||
} else {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::TooManyDescriptors));
|
|
||||||
};
|
|
||||||
|
|
||||||
if data_len < DISCARD_WZ_SEG_SIZE {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::DescriptorLengthTooSmall));
|
|
||||||
}
|
|
||||||
if data_len > DISCARD_WZ_MAX_PAYLOAD {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::TooManySegments(
|
|
||||||
data_len.div_ceil(DISCARD_WZ_SEG_SIZE),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut discard_sector = [0u8; 8];
|
|
||||||
let mut discard_num_sectors = [0u8; 4];
|
|
||||||
let mut discard_flags = [0u8; 4];
|
|
||||||
|
|
||||||
let sector_addr = data_addr.checked_add(DISCARD_WZ_SECTOR_OFFSET).unwrap();
|
|
||||||
mem.read_slice(&mut discard_sector, sector_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let num_sectors_addr = data_addr
|
|
||||||
.checked_add(DISCARD_WZ_NUM_SECTORS_OFFSET)
|
|
||||||
.unwrap();
|
|
||||||
mem.read_slice(&mut discard_num_sectors, num_sectors_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let flags_addr = data_addr.checked_add(DISCARD_WZ_FLAGS_OFFSET).unwrap();
|
|
||||||
mem.read_slice(&mut discard_flags, flags_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let discard_flags = u32::from_le_bytes(discard_flags);
|
|
||||||
// Per virtio spec v1.2 reject discard if any flag is set, including unmap.
|
|
||||||
if discard_flags != 0 {
|
|
||||||
warn!("Unsupported flags {discard_flags:#x} in discard request");
|
|
||||||
return Err(ExecuteError::UnsupportedFlags {
|
|
||||||
request_type: VIRTIO_BLK_T_DISCARD,
|
|
||||||
flags: discard_flags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let discard_sector = u64::from_le_bytes(discard_sector);
|
|
||||||
|
|
||||||
if discard_sector == 0 && disable_sector0_writes {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let discard_num_sectors = u32::from_le_bytes(discard_num_sectors);
|
|
||||||
|
|
||||||
let top = discard_sector
|
|
||||||
.checked_add(discard_num_sectors as u64)
|
|
||||||
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
|
||||||
if top > disk_nsectors {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let discard_offset = discard_sector * SECTOR_SIZE;
|
|
||||||
let discard_length = (discard_num_sectors as u64) * SECTOR_SIZE;
|
|
||||||
|
|
||||||
disk_image
|
|
||||||
.punch_hole(discard_offset, discard_length, user_data)
|
|
||||||
.map_err(ExecuteError::AsyncPunchHole)?;
|
|
||||||
}
|
|
||||||
RequestType::WriteZeroes => {
|
|
||||||
let (data_addr, data_len) = if self.data_descriptors.len() == 1 {
|
|
||||||
(self.data_descriptors[0].0, self.data_descriptors[0].1)
|
|
||||||
} else {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::TooManyDescriptors));
|
|
||||||
};
|
|
||||||
|
|
||||||
if data_len < DISCARD_WZ_SEG_SIZE {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::DescriptorLengthTooSmall));
|
|
||||||
}
|
|
||||||
if data_len > DISCARD_WZ_MAX_PAYLOAD {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::TooManySegments(
|
|
||||||
data_len.div_ceil(DISCARD_WZ_SEG_SIZE),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut wz_sector = [0u8; 8];
|
|
||||||
let mut wz_num_sectors = [0u8; 4];
|
|
||||||
let mut wz_flags = [0u8; 4];
|
|
||||||
|
|
||||||
let sector_addr = data_addr.checked_add(DISCARD_WZ_SECTOR_OFFSET).unwrap();
|
|
||||||
mem.read_slice(&mut wz_sector, sector_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let num_sectors_addr = data_addr
|
|
||||||
.checked_add(DISCARD_WZ_NUM_SECTORS_OFFSET)
|
|
||||||
.unwrap();
|
|
||||||
mem.read_slice(&mut wz_num_sectors, num_sectors_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let flags_addr = data_addr.checked_add(DISCARD_WZ_FLAGS_OFFSET).unwrap();
|
|
||||||
mem.read_slice(&mut wz_flags, flags_addr)
|
|
||||||
.map_err(ExecuteError::Read)?;
|
|
||||||
|
|
||||||
let wz_sector = u64::from_le_bytes(wz_sector);
|
|
||||||
let wz_num_sectors = u32::from_le_bytes(wz_num_sectors);
|
|
||||||
|
|
||||||
let wz_flags = u32::from_le_bytes(wz_flags);
|
|
||||||
// Per virtio spec v1.2 reject write zeroes if any unknown flag is set.
|
|
||||||
if (wz_flags & !VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP) != 0 {
|
|
||||||
warn!("Unsupported flags {wz_flags:#x} in write zeroes request");
|
|
||||||
return Err(ExecuteError::UnsupportedFlags {
|
|
||||||
request_type: VIRTIO_BLK_T_WRITE_ZEROES,
|
|
||||||
flags: wz_flags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let wz_offset = wz_sector * SECTOR_SIZE;
|
|
||||||
if wz_offset == 0 && disable_sector0_writes {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let top = wz_sector
|
|
||||||
.checked_add(wz_num_sectors as u64)
|
|
||||||
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
|
||||||
if top > disk_nsectors {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let wz_length = (wz_num_sectors as u64) * SECTOR_SIZE;
|
|
||||||
|
|
||||||
if wz_flags & VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP != 0 {
|
|
||||||
disk_image
|
|
||||||
.punch_hole(wz_offset, wz_length, user_data)
|
|
||||||
.map_err(ExecuteError::AsyncPunchHole)?;
|
|
||||||
} else {
|
|
||||||
disk_image
|
|
||||||
.write_zeroes(wz_offset, wz_length, user_data)
|
|
||||||
.map_err(ExecuteError::AsyncWriteZeroes)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn complete_async<B: Bitmap + 'static>(
|
|
||||||
&mut self,
|
|
||||||
mem: &vm_memory::GuestMemoryMmap<B>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
for aligned_op in self.aligned_operations.drain(..) {
|
|
||||||
// We need to perform the copy after the data has been read inside
|
|
||||||
// the aligned buffer in case we're reading data in.
|
|
||||||
if self.request_type == RequestType::In {
|
|
||||||
mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr())
|
|
||||||
.map_err(Error::GuestMemory)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn data_descriptors(
|
|
||||||
&self,
|
|
||||||
) -> &SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]> {
|
|
||||||
&self.data_descriptors
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn status_addr(&self) -> GuestAddress {
|
|
||||||
self.status_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn start(&self) -> Instant {
|
|
||||||
self.start
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn sector(&self) -> u64 {
|
|
||||||
self.sector
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn request_type(&self) -> RequestType {
|
|
||||||
self.request_type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For In and Out requests, checks that the descriptors collectively fit in a backing disk of
|
|
||||||
/// the given size. Returns `Ok(())` if they fit, or `ExecuteError::BadRequest` otherwise.
|
|
||||||
fn check_data_bounds(&self, disk_nsectors: u64) -> Result<(), ExecuteError> {
|
|
||||||
if !matches!(self.request_type, RequestType::In | RequestType::Out) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let mut total_bytes: u64 = 0;
|
|
||||||
for (_, data_len) in &self.data_descriptors {
|
|
||||||
total_bytes = total_bytes
|
|
||||||
.checked_add(u64::from(*data_len))
|
|
||||||
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
|
||||||
}
|
|
||||||
if total_bytes == 0 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let total_sectors = total_bytes.div_ceil(SECTOR_SIZE);
|
|
||||||
let end_sector = self
|
|
||||||
.sector
|
|
||||||
.checked_add(total_sectors)
|
|
||||||
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
|
|
||||||
if end_sector > disk_nsectors {
|
|
||||||
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -135,7 +135,7 @@ impl Header {
|
|||||||
.map_err(VhdxHeaderError::ReadHeader)?;
|
.map_err(VhdxHeaderError::ReadHeader)?;
|
||||||
|
|
||||||
// SAFETY: buffer is of correct size and has been successfully filled.
|
// SAFETY: buffer is of correct size and has been successfully filled.
|
||||||
let header: Header = unsafe { *(buffer.as_ptr().cast()) };
|
let header = unsafe { *(buffer.as_ptr() as *mut Header) };
|
||||||
if header.signature != HEADER_SIGN {
|
if header.signature != HEADER_SIGN {
|
||||||
return Err(VhdxHeaderError::InvalidHeaderSign);
|
return Err(VhdxHeaderError::InvalidHeaderSign);
|
||||||
}
|
}
|
||||||
@@ -151,8 +151,9 @@ impl Header {
|
|||||||
/// Converts the header structure into a buffer
|
/// Converts the header structure into a buffer
|
||||||
fn write_to_buffer(&self, buffer: &mut [u8; HEADER_SIZE as usize]) {
|
fn write_to_buffer(&self, buffer: &mut [u8; HEADER_SIZE as usize]) {
|
||||||
// SAFETY: self is a valid header.
|
// SAFETY: self is a valid header.
|
||||||
let reference =
|
let reference = unsafe {
|
||||||
unsafe { std::slice::from_raw_parts((&raw const *self).cast(), HEADER_SIZE as usize) };
|
std::slice::from_raw_parts(self as *const Header as *const u8, HEADER_SIZE as usize)
|
||||||
|
};
|
||||||
*buffer = reference.try_into().unwrap();
|
*buffer = reference.try_into().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +222,7 @@ impl RegionTableHeader {
|
|||||||
.map_err(VhdxHeaderError::ReadRegionTableHeader)?;
|
.map_err(VhdxHeaderError::ReadRegionTableHeader)?;
|
||||||
|
|
||||||
// SAFETY: buffer is of correct size and has been successfully filled.
|
// SAFETY: buffer is of correct size and has been successfully filled.
|
||||||
let region_table_header: RegionTableHeader = unsafe { *(buffer.as_ptr().cast()) };
|
let region_table_header = unsafe { *(buffer.as_ptr() as *mut RegionTableHeader) };
|
||||||
if region_table_header.signature != REGION_SIGN {
|
if region_table_header.signature != REGION_SIGN {
|
||||||
return Err(VhdxHeaderError::InvalidRegionSign);
|
return Err(VhdxHeaderError::InvalidRegionSign);
|
||||||
}
|
}
|
||||||
@@ -339,7 +340,7 @@ impl RegionTableEntry {
|
|||||||
pub fn new(buffer: &[u8]) -> Result<RegionTableEntry> {
|
pub fn new(buffer: &[u8]) -> Result<RegionTableEntry> {
|
||||||
assert!(buffer.len() == std::mem::size_of::<RegionTableEntry>());
|
assert!(buffer.len() == std::mem::size_of::<RegionTableEntry>());
|
||||||
// SAFETY: the assertion above makes sure the buffer size is correct.
|
// SAFETY: the assertion above makes sure the buffer size is correct.
|
||||||
let mut region_table_entry: RegionTableEntry = unsafe { *(buffer.as_ptr().cast()) };
|
let mut region_table_entry = unsafe { *(buffer.as_ptr() as *mut RegionTableEntry) };
|
||||||
|
|
||||||
let uuid = crate::vhdx::uuid_from_guid(buffer);
|
let uuid = crate::vhdx::uuid_from_guid(buffer);
|
||||||
region_table_entry.guid = uuid;
|
region_table_entry.guid = uuid;
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ impl MetadataTableHeader {
|
|||||||
pub fn new(buffer: &[u8]) -> Result<MetadataTableHeader> {
|
pub fn new(buffer: &[u8]) -> Result<MetadataTableHeader> {
|
||||||
assert!(buffer.len() == std::mem::size_of::<MetadataTableHeader>());
|
assert!(buffer.len() == std::mem::size_of::<MetadataTableHeader>());
|
||||||
// SAFETY: the assertion above makes sure the buffer size is correct.
|
// SAFETY: the assertion above makes sure the buffer size is correct.
|
||||||
let metadata_table_header: MetadataTableHeader = unsafe { *(buffer.as_ptr().cast()) };
|
let metadata_table_header = unsafe { *(buffer.as_ptr() as *mut MetadataTableHeader) };
|
||||||
|
|
||||||
if metadata_table_header.signature != METADATA_SIGN {
|
if metadata_table_header.signature != METADATA_SIGN {
|
||||||
return Err(VhdxMetadataError::InvalidMetadataSign);
|
return Err(VhdxMetadataError::InvalidMetadataSign);
|
||||||
@@ -313,7 +313,7 @@ impl MetadataTableEntry {
|
|||||||
fn new(buffer: &[u8]) -> Result<MetadataTableEntry> {
|
fn new(buffer: &[u8]) -> Result<MetadataTableEntry> {
|
||||||
assert!(buffer.len() == std::mem::size_of::<MetadataTableEntry>());
|
assert!(buffer.len() == std::mem::size_of::<MetadataTableEntry>());
|
||||||
// SAFETY: the assertion above makes sure the buffer size is correct.
|
// SAFETY: the assertion above makes sure the buffer size is correct.
|
||||||
let mut metadata_table_entry: MetadataTableEntry = unsafe { *(buffer.as_ptr().cast()) };
|
let mut metadata_table_entry = unsafe { *(buffer.as_ptr() as *mut MetadataTableEntry) };
|
||||||
|
|
||||||
let uuid = crate::vhdx::uuid_from_guid(buffer);
|
let uuid = crate::vhdx::uuid_from_guid(buffer);
|
||||||
metadata_table_entry.item_id = uuid;
|
metadata_table_entry.item_id = uuid;
|
||||||
|
|||||||
@@ -5,115 +5,67 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError};
|
use crate::async_io::{
|
||||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
AsyncIo, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||||
use crate::vhdx::{Vhdx, VhdxError};
|
};
|
||||||
use crate::{AsyncAdaptor, BlockBackend, Error, disk_file};
|
use crate::vhdx::{Result as VhdxResult, Vhdx};
|
||||||
|
use crate::{AsyncAdaptor, BlockBackend, Error};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VhdxDiskSync {
|
pub struct VhdxDiskSync {
|
||||||
// FIXME: The Mutex serializes all VHDX I/O operations across queues, which
|
vhdx_file: Vhdx,
|
||||||
// is necessary for correctness but eliminates any parallelism benefit from
|
|
||||||
// multiqueue. Vhdx::clone() shares the underlying file description across
|
|
||||||
// threads, so concurrent I/O from multiple queues races on the file offset
|
|
||||||
// causing data corruption.
|
|
||||||
//
|
|
||||||
// A proper fix would require restructuring the VHDX I/O path so that data
|
|
||||||
// operations can proceed in parallel with independent file descriptors.
|
|
||||||
vhdx_file: Arc<Mutex<Vhdx>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VhdxDiskSync {
|
impl VhdxDiskSync {
|
||||||
pub fn new(f: File) -> BlockResult<Self> {
|
pub fn new(f: File) -> VhdxResult<Self> {
|
||||||
Ok(VhdxDiskSync {
|
Ok(VhdxDiskSync {
|
||||||
vhdx_file: Arc::new(Mutex::new(Vhdx::new(f).map_err(|e| {
|
vhdx_file: Vhdx::new(f)?,
|
||||||
let kind = match &e {
|
|
||||||
VhdxError::NotVhdx(_)
|
|
||||||
| VhdxError::ParseVhdxHeader(_)
|
|
||||||
| VhdxError::ParseVhdxMetadata(_)
|
|
||||||
| VhdxError::ParseVhdxRegionEntry(_) => BlockErrorKind::InvalidFormat,
|
|
||||||
VhdxError::ReadBatEntry(_) => BlockErrorKind::CorruptImage,
|
|
||||||
VhdxError::ReadFailed(_) | VhdxError::WriteFailed(_) => BlockErrorKind::Io,
|
|
||||||
};
|
|
||||||
BlockError::new(kind, e).with_op(ErrorOp::Open)
|
|
||||||
})?)),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl disk_file::DiskSize for VhdxDiskSync {
|
impl DiskFile for VhdxDiskSync {
|
||||||
fn logical_size(&self) -> BlockResult<u64> {
|
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
Ok(self.vhdx_file.lock().unwrap().virtual_disk_size())
|
Ok(self.vhdx_file.virtual_disk_size())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::PhysicalSize for VhdxDiskSync {
|
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||||
fn physical_size(&self) -> BlockResult<u64> {
|
self.vhdx_file.physical_size().map_err(|e| {
|
||||||
self.vhdx_file
|
let io_inner = match e {
|
||||||
.lock()
|
Error::GetFileMetadata(e) => e,
|
||||||
.unwrap()
|
_ => unreachable!(),
|
||||||
.physical_size()
|
};
|
||||||
.map_err(|e| match e {
|
DiskFileError::Size(io_inner)
|
||||||
Error::GetFileMetadata(io) => {
|
})
|
||||||
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
|
|
||||||
}
|
|
||||||
_ => unreachable!("unexpected error from Vhdx::physical_size(): {e}"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFd for VhdxDiskSync {
|
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
Ok(
|
||||||
BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd())
|
Box::new(VhdxSync::new(self.vhdx_file.clone()).map_err(DiskFileError::NewAsyncIo)?)
|
||||||
}
|
as Box<dyn AsyncIo>,
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::Geometry for VhdxDiskSync {}
|
|
||||||
|
|
||||||
impl disk_file::SparseCapable for VhdxDiskSync {}
|
|
||||||
|
|
||||||
impl disk_file::Resizable for VhdxDiskSync {
|
|
||||||
fn resize(&mut self, _size: u64) -> BlockResult<()> {
|
|
||||||
Err(BlockError::new(
|
|
||||||
BlockErrorKind::UnsupportedFeature,
|
|
||||||
DiskFileError::ResizeError(std::io::Error::other("resize not supported for VHDX")),
|
|
||||||
)
|
)
|
||||||
.with_op(ErrorOp::Resize))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl disk_file::DiskFile for VhdxDiskSync {}
|
|
||||||
|
|
||||||
impl disk_file::AsyncDiskFile for VhdxDiskSync {
|
|
||||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
|
||||||
Ok(Box::new(VhdxDiskSync {
|
|
||||||
vhdx_file: Arc::clone(&self.vhdx_file),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_async_io(&self, _ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||||
Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file))))
|
BorrowedDiskFd::new(self.vhdx_file.as_raw_fd())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VhdxSync {
|
pub struct VhdxSync {
|
||||||
vhdx_file: Arc<Mutex<Vhdx>>,
|
vhdx_file: Vhdx,
|
||||||
eventfd: EventFd,
|
eventfd: EventFd,
|
||||||
completion_list: VecDeque<(u64, i32)>,
|
completion_list: VecDeque<(u64, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VhdxSync {
|
impl VhdxSync {
|
||||||
pub fn new(vhdx_file: Arc<Mutex<Vhdx>>) -> Self {
|
pub fn new(vhdx_file: Vhdx) -> std::io::Result<Self> {
|
||||||
VhdxSync {
|
Ok(VhdxSync {
|
||||||
vhdx_file,
|
vhdx_file,
|
||||||
eventfd: EventFd::new(libc::EFD_NONBLOCK)
|
eventfd: EventFd::new(libc::EFD_NONBLOCK)?,
|
||||||
.expect("Failed creating EventFd for VhdxSync"),
|
|
||||||
completion_list: VecDeque::new(),
|
completion_list: VecDeque::new(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +82,7 @@ impl AsyncIo for VhdxSync {
|
|||||||
iovecs: &[libc::iovec],
|
iovecs: &[libc::iovec],
|
||||||
user_data: u64,
|
user_data: u64,
|
||||||
) -> AsyncIoResult<()> {
|
) -> AsyncIoResult<()> {
|
||||||
self.vhdx_file.lock().unwrap().read_vectored_sync(
|
self.vhdx_file.read_vectored_sync(
|
||||||
offset,
|
offset,
|
||||||
iovecs,
|
iovecs,
|
||||||
user_data,
|
user_data,
|
||||||
@@ -145,7 +97,7 @@ impl AsyncIo for VhdxSync {
|
|||||||
iovecs: &[libc::iovec],
|
iovecs: &[libc::iovec],
|
||||||
user_data: u64,
|
user_data: u64,
|
||||||
) -> AsyncIoResult<()> {
|
) -> AsyncIoResult<()> {
|
||||||
self.vhdx_file.lock().unwrap().write_vectored_sync(
|
self.vhdx_file.write_vectored_sync(
|
||||||
offset,
|
offset,
|
||||||
iovecs,
|
iovecs,
|
||||||
user_data,
|
user_data,
|
||||||
@@ -155,26 +107,11 @@ impl AsyncIo for VhdxSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||||
self.vhdx_file.lock().unwrap().fsync_sync(
|
self.vhdx_file
|
||||||
user_data,
|
.fsync_sync(user_data, &self.eventfd, &mut self.completion_list)
|
||||||
&self.eventfd,
|
|
||||||
&mut self.completion_list,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||||
self.completion_list.pop_front()
|
self.completion_list.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::PunchHole(std::io::Error::other(
|
|
||||||
"punch_hole not supported for VHDX",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
|
||||||
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
|
|
||||||
"write_zeroes not supported for VHDX",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,15 @@ edition = "2024"
|
|||||||
homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
|
homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
|
||||||
license = "Apache-2.0 AND BSD-3-Clause"
|
license = "Apache-2.0 AND BSD-3-Clause"
|
||||||
name = "cloud-hypervisor"
|
name = "cloud-hypervisor"
|
||||||
rust-version.workspace = true
|
version = "50.1.0"
|
||||||
version = "52.0.0"
|
# Minimum buildable version:
|
||||||
|
# Keep in sync with version in .github/workflows/build.yaml
|
||||||
|
# Policy on MSRV (see #4318):
|
||||||
|
# Can only be bumped if satisfying any of the following:
|
||||||
|
# a.) A dependency requires it,
|
||||||
|
# b.) If we want to use a new feature and that MSRV is at least 6 months old,
|
||||||
|
# c.) There is a security issue that is addressed by the toolchain update.
|
||||||
|
rust-version = "1.89.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
@@ -19,7 +26,6 @@ env_logger = { workspace = true }
|
|||||||
epoll = { workspace = true }
|
epoll = { workspace = true }
|
||||||
event_monitor = { path = "../event_monitor" }
|
event_monitor = { path = "../event_monitor" }
|
||||||
hypervisor = { path = "../hypervisor" }
|
hypervisor = { path = "../hypervisor" }
|
||||||
jiff = { workspace = true }
|
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
log = { workspace = true, features = ["std"] }
|
log = { workspace = true, features = ["std"] }
|
||||||
option_parser = { path = "../option_parser" }
|
option_parser = { path = "../option_parser" }
|
||||||
@@ -32,7 +38,7 @@ tracer = { path = "../tracer" }
|
|||||||
vm-memory = { workspace = true }
|
vm-memory = { workspace = true }
|
||||||
vmm = { path = "../vmm" }
|
vmm = { path = "../vmm" }
|
||||||
vmm-sys-util = { workspace = true }
|
vmm-sys-util = { workspace = true }
|
||||||
zbus = { version = "5.15.0", optional = true }
|
zbus = { version = "5.7.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
block = { path = "../block" }
|
block = { path = "../block" }
|
||||||
|
|||||||
@@ -16,16 +16,14 @@ use api_client::{
|
|||||||
Error as ApiClientError, simple_api_command, simple_api_command_with_fds,
|
Error as ApiClientError, simple_api_command, simple_api_command_with_fds,
|
||||||
simple_api_full_command,
|
simple_api_full_command,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "dbus_api")]
|
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||||
use clap::ArgAction;
|
|
||||||
use clap::{Arg, ArgMatches, Command};
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use option_parser::{ByteSized, ByteSizedParseError};
|
use option_parser::{ByteSized, ByteSizedParseError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use vmm::config::RestoreConfig;
|
use vmm::config::RestoreConfig;
|
||||||
use vmm::vm_config::{
|
use vmm::vm_config::{
|
||||||
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
|
DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig,
|
||||||
UserDeviceConfig, VdpaConfig, VsockConfig,
|
VsockConfig,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "dbus_api")]
|
#[cfg(feature = "dbus_api")]
|
||||||
use zbus::{proxy, zvariant::Optional};
|
use zbus::{proxy, zvariant::Optional};
|
||||||
@@ -51,8 +49,6 @@ enum Error {
|
|||||||
AddDiskConfig(#[source] vmm::config::Error),
|
AddDiskConfig(#[source] vmm::config::Error),
|
||||||
#[error("Error parsing filesystem syntax")]
|
#[error("Error parsing filesystem syntax")]
|
||||||
AddFsConfig(#[source] vmm::config::Error),
|
AddFsConfig(#[source] vmm::config::Error),
|
||||||
#[error("Error parsing generic vhost-user syntax")]
|
|
||||||
AddGenericVhostUserConfig(#[source] vmm::config::Error),
|
|
||||||
#[error("Error parsing persistent memory syntax")]
|
#[error("Error parsing persistent memory syntax")]
|
||||||
AddPmemConfig(#[source] vmm::config::Error),
|
AddPmemConfig(#[source] vmm::config::Error),
|
||||||
#[error("Error parsing network syntax")]
|
#[error("Error parsing network syntax")]
|
||||||
@@ -71,8 +67,6 @@ enum Error {
|
|||||||
ReadingFile(#[source] std::io::Error),
|
ReadingFile(#[source] std::io::Error),
|
||||||
#[error("Invalid disk size")]
|
#[error("Invalid disk size")]
|
||||||
InvalidDiskSize(#[source] ByteSizedParseError),
|
InvalidDiskSize(#[source] ByteSizedParseError),
|
||||||
#[error("Error parsing send migration configuration")]
|
|
||||||
SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TargetApi<'a> {
|
enum TargetApi<'a> {
|
||||||
@@ -89,10 +83,6 @@ trait DBusApi1 {
|
|||||||
fn vm_add_device(&self, device_config: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_device(&self, device_config: &str) -> zbus::Result<Optional<String>>;
|
||||||
fn vm_add_disk(&self, disk_config: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_disk(&self, disk_config: &str) -> zbus::Result<Optional<String>>;
|
||||||
fn vm_add_fs(&self, fs_config: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_fs(&self, fs_config: &str) -> zbus::Result<Optional<String>>;
|
||||||
fn vm_add_generic_vhost_user(
|
|
||||||
&self,
|
|
||||||
generic_vhost_user_config: &str,
|
|
||||||
) -> zbus::Result<Optional<String>>;
|
|
||||||
fn vm_add_net(&self, net_config: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_net(&self, net_config: &str) -> zbus::Result<Optional<String>>;
|
||||||
fn vm_add_pmem(&self, pmem_config: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_pmem(&self, pmem_config: &str) -> zbus::Result<Optional<String>>;
|
||||||
fn vm_add_user_device(&self, vm_add_user_device: &str) -> zbus::Result<Optional<String>>;
|
fn vm_add_user_device(&self, vm_add_user_device: &str) -> zbus::Result<Optional<String>>;
|
||||||
@@ -165,10 +155,6 @@ impl<'a> DBusApi1ProxyBlocking<'a> {
|
|||||||
self.print_response(self.vm_add_fs(fs_config))
|
self.print_response(self.vm_add_fs(fs_config))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_vm_add_generic_vhost_user(&self, generic_vhost_user_config: &str) -> ApiResult {
|
|
||||||
self.print_response(self.vm_add_generic_vhost_user(generic_vhost_user_config))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn api_vm_add_net(&self, net_config: &str) -> ApiResult {
|
fn api_vm_add_net(&self, net_config: &str) -> ApiResult {
|
||||||
self.print_response(self.vm_add_net(net_config))
|
self.print_response(self.vm_add_net(net_config))
|
||||||
}
|
}
|
||||||
@@ -412,22 +398,6 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu
|
|||||||
simple_api_command(socket, "PUT", "add-fs", Some(&fs_config))
|
simple_api_command(socket, "PUT", "add-fs", Some(&fs_config))
|
||||||
.map_err(Error::HttpApiClient)
|
.map_err(Error::HttpApiClient)
|
||||||
}
|
}
|
||||||
Some("add-generic-vhost-user") => {
|
|
||||||
let device_config = add_generic_vhost_user_config(
|
|
||||||
matches
|
|
||||||
.subcommand_matches("add-generic-vhost-user")
|
|
||||||
.unwrap()
|
|
||||||
.get_one::<String>("generic_vhost_user_config")
|
|
||||||
.unwrap(),
|
|
||||||
)?;
|
|
||||||
simple_api_command(
|
|
||||||
socket,
|
|
||||||
"PUT",
|
|
||||||
"add-generic-vhost-user",
|
|
||||||
Some(&device_config),
|
|
||||||
)
|
|
||||||
.map_err(Error::HttpApiClient)
|
|
||||||
}
|
|
||||||
Some("add-pmem") => {
|
Some("add-pmem") => {
|
||||||
let pmem_config = add_pmem_config(
|
let pmem_config = add_pmem_config(
|
||||||
matches
|
matches
|
||||||
@@ -523,7 +493,11 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_one::<String>("send_migration_config")
|
.get_one::<String>("send_migration_config")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)?;
|
matches
|
||||||
|
.subcommand_matches("send-migration")
|
||||||
|
.unwrap()
|
||||||
|
.get_flag("send_migration_local"),
|
||||||
|
);
|
||||||
simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data))
|
simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data))
|
||||||
.map_err(Error::HttpApiClient)
|
.map_err(Error::HttpApiClient)
|
||||||
}
|
}
|
||||||
@@ -646,16 +620,6 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
|
|||||||
)?;
|
)?;
|
||||||
proxy.api_vm_add_fs(&fs_config)
|
proxy.api_vm_add_fs(&fs_config)
|
||||||
}
|
}
|
||||||
Some("add-generic-vhost-user") => {
|
|
||||||
let generic_vhost_user_config = add_generic_vhost_user_config(
|
|
||||||
matches
|
|
||||||
.subcommand_matches("add-generic-vhost-user")
|
|
||||||
.unwrap()
|
|
||||||
.get_one::<String>("generic_vhost_user_config")
|
|
||||||
.unwrap(),
|
|
||||||
)?;
|
|
||||||
proxy.api_vm_add_generic_vhost_user(&generic_vhost_user_config)
|
|
||||||
}
|
|
||||||
Some("add-pmem") => {
|
Some("add-pmem") => {
|
||||||
let pmem_config = add_pmem_config(
|
let pmem_config = add_pmem_config(
|
||||||
matches
|
matches
|
||||||
@@ -743,7 +707,11 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_one::<String>("send_migration_config")
|
.get_one::<String>("send_migration_config")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)?;
|
matches
|
||||||
|
.subcommand_matches("send-migration")
|
||||||
|
.unwrap()
|
||||||
|
.get_flag("send_migration_local"),
|
||||||
|
);
|
||||||
proxy.api_vm_send_migration(&send_migration_data)
|
proxy.api_vm_send_migration(&send_migration_data)
|
||||||
}
|
}
|
||||||
Some("receive-migration") => {
|
Some("receive-migration") => {
|
||||||
@@ -867,14 +835,6 @@ fn add_fs_config(config: &str) -> Result<String, Error> {
|
|||||||
Ok(fs_config)
|
Ok(fs_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_generic_vhost_user_config(config: &str) -> Result<String, Error> {
|
|
||||||
let generic_vhost_user_config =
|
|
||||||
GenericVhostUserConfig::parse(config).map_err(Error::AddGenericVhostUserConfig)?;
|
|
||||||
let generic_vhost_user_config = serde_json::to_string(&generic_vhost_user_config).unwrap();
|
|
||||||
|
|
||||||
Ok(generic_vhost_user_config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_pmem_config(config: &str) -> Result<String, Error> {
|
fn add_pmem_config(config: &str) -> Result<String, Error> {
|
||||||
let pmem_config = PmemConfig::parse(config).map_err(Error::AddPmemConfig)?;
|
let pmem_config = PmemConfig::parse(config).map_err(Error::AddPmemConfig)?;
|
||||||
let pmem_config = serde_json::to_string(&pmem_config).unwrap();
|
let pmem_config = serde_json::to_string(&pmem_config).unwrap();
|
||||||
@@ -949,11 +909,13 @@ fn receive_migration_data(url: &str) -> String {
|
|||||||
serde_json::to_string(&receive_migration_data).unwrap()
|
serde_json::to_string(&receive_migration_data).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_migration_data(config: &str) -> Result<String, Error> {
|
fn send_migration_data(url: &str, local: bool) -> String {
|
||||||
let send_migration_data =
|
let send_migration_data = vmm::api::VmSendMigrationData {
|
||||||
vmm::api::VmSendMigrationData::parse(config).map_err(Error::SendMigrationConfig)?;
|
destination_url: url.to_owned(),
|
||||||
let send_migration_config = serde_json::to_string(&send_migration_data).unwrap();
|
local,
|
||||||
Ok(send_migration_config)
|
};
|
||||||
|
|
||||||
|
serde_json::to_string(&send_migration_data).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_data(path: &str) -> Result<String, Error> {
|
fn create_data(path: &str) -> Result<String, Error> {
|
||||||
@@ -1019,13 +981,6 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
|
|||||||
.index(1)
|
.index(1)
|
||||||
.help(vmm::vm_config::FsConfig::SYNTAX),
|
.help(vmm::vm_config::FsConfig::SYNTAX),
|
||||||
),
|
),
|
||||||
Command::new("add-generic-vhost-user")
|
|
||||||
.about("Add generic vhost-user device")
|
|
||||||
.arg(
|
|
||||||
Arg::new("generic_vhost_user_config")
|
|
||||||
.index(1)
|
|
||||||
.help(vmm::vm_config::GenericVhostUserConfig::SYNTAX),
|
|
||||||
),
|
|
||||||
Command::new("add-net")
|
Command::new("add-net")
|
||||||
.about("Add network device")
|
.about("Add network device")
|
||||||
.arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)),
|
.arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)),
|
||||||
@@ -1135,7 +1090,13 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
|
|||||||
.arg(
|
.arg(
|
||||||
Arg::new("send_migration_config")
|
Arg::new("send_migration_config")
|
||||||
.index(1)
|
.index(1)
|
||||||
.help(vmm::api::VmSendMigrationData::SYNTAX),
|
.help("<destination_url>"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("send_migration_local")
|
||||||
|
.long("local")
|
||||||
|
.num_args(0)
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
),
|
),
|
||||||
Command::new("shutdown").about("Shutdown the VM"),
|
Command::new("shutdown").about("Shutdown the VM"),
|
||||||
Command::new("shutdown-vmm").about("Shutdown the VMM"),
|
Command::new("shutdown-vmm").about("Shutdown the VMM"),
|
||||||
|
|||||||
@@ -1,436 +0,0 @@
|
|||||||
// Copyright © 2026 Cloud Hypervisor Contributors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
//
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Unterminated '{{' in format string")]
|
|
||||||
UnterminatedBrace,
|
|
||||||
#[error("Unmatched '}}' in format string")]
|
|
||||||
UnmatchedBrace,
|
|
||||||
#[error("Unknown format token '{{{0}}}'")]
|
|
||||||
UnknownToken(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Token {
|
|
||||||
Literal(String),
|
|
||||||
BootTime,
|
|
||||||
/// Wallclock using RFC 3339 formatting.
|
|
||||||
WallClock,
|
|
||||||
Pid,
|
|
||||||
Tid,
|
|
||||||
Thread,
|
|
||||||
Level,
|
|
||||||
Location,
|
|
||||||
Msg,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Token {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"boottime" => Ok(Self::BootTime),
|
|
||||||
"wallclock" => Ok(Self::WallClock),
|
|
||||||
"pid" => Ok(Self::Pid),
|
|
||||||
"tid" => Ok(Self::Tid),
|
|
||||||
"thread" => Ok(Self::Thread),
|
|
||||||
"level" => Ok(Self::Level),
|
|
||||||
"location" => Ok(Self::Location),
|
|
||||||
"msg" => Ok(Self::Msg),
|
|
||||||
_ => Err(Error::UnknownToken(s.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_format(fmt: &str) -> Result<Vec<Token>, Error> {
|
|
||||||
let mut tokens = Vec::new();
|
|
||||||
let mut literal = String::new();
|
|
||||||
let mut chars = fmt.chars().peekable();
|
|
||||||
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
match c {
|
|
||||||
'{' => {
|
|
||||||
if chars.peek() == Some(&'{') {
|
|
||||||
chars.next();
|
|
||||||
literal.push('{');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !literal.is_empty() {
|
|
||||||
tokens.push(Token::Literal(std::mem::take(&mut literal)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut name = String::new();
|
|
||||||
loop {
|
|
||||||
match chars.next() {
|
|
||||||
Some('}') => break,
|
|
||||||
Some(ch) => name.push(ch),
|
|
||||||
None => return Err(Error::UnterminatedBrace),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.push(name.parse()?);
|
|
||||||
}
|
|
||||||
'}' => {
|
|
||||||
if chars.peek() == Some(&'}') {
|
|
||||||
chars.next();
|
|
||||||
literal.push('}');
|
|
||||||
} else {
|
|
||||||
return Err(Error::UnmatchedBrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => literal.push(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !literal.is_empty() {
|
|
||||||
tokens.push(Token::Literal(literal));
|
|
||||||
}
|
|
||||||
Ok(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DEFAULT_FORMAT: &str =
|
|
||||||
"cloud-hypervisor: {boottime}s: <{thread}> {level}:{location} -- {msg}";
|
|
||||||
|
|
||||||
pub struct Logger {
|
|
||||||
output: Mutex<Box<dyn Write + Send>>,
|
|
||||||
start: Instant,
|
|
||||||
pid: u32,
|
|
||||||
tokens: Vec<Token>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Logger {
|
|
||||||
pub fn new(output: Box<dyn Write + Send>, format: &str) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
output: Mutex::new(output),
|
|
||||||
start: Instant::now(),
|
|
||||||
pid: std::process::id(),
|
|
||||||
tokens: parse_format(format)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl log::Log for Logger {
|
|
||||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(&self, record: &log::Record) {
|
|
||||||
if !self.enabled(record.metadata()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let duration_s = Instant::now().duration_since(self.start).as_secs_f32();
|
|
||||||
let mut out = self.output.lock().unwrap();
|
|
||||||
for token in &self.tokens {
|
|
||||||
let _ = match token {
|
|
||||||
Token::Literal(s) => out.write_all(s.as_bytes()),
|
|
||||||
// 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned
|
|
||||||
Token::BootTime => write!(&mut *out, "{duration_s:>10.6?}"),
|
|
||||||
Token::WallClock => {
|
|
||||||
write!(out, "{:.6}", jiff::Timestamp::now())
|
|
||||||
}
|
|
||||||
Token::Pid => write!(&mut *out, "{}", self.pid),
|
|
||||||
// SAFETY: gettid(2) always succeeds
|
|
||||||
Token::Tid => write!(&mut *out, "{}", unsafe { libc::gettid() }),
|
|
||||||
Token::Thread => write!(
|
|
||||||
&mut *out,
|
|
||||||
"{}",
|
|
||||||
std::thread::current().name().unwrap_or("anonymous")
|
|
||||||
),
|
|
||||||
Token::Level => write!(&mut *out, "{}", record.level()),
|
|
||||||
Token::Location => match (record.file(), record.line()) {
|
|
||||||
(Some(file), Some(line)) => write!(&mut *out, "{file}:{line}"),
|
|
||||||
_ => write!(&mut *out, "{}", record.target()),
|
|
||||||
},
|
|
||||||
Token::Msg => write!(&mut *out, "{}", record.args()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let _ = out.write_all(b"\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::io;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use log::Log;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// A `Write` sink that appends to a shared byte buffer so tests can
|
|
||||||
/// inspect what the logger wrote.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
|
|
||||||
|
|
||||||
impl SharedBuffer {
|
|
||||||
fn contents(&self) -> String {
|
|
||||||
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for SharedBuffer {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.0.lock().unwrap().extend_from_slice(buf);
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(tokens: &[Token]) -> String {
|
|
||||||
tokens
|
|
||||||
.iter()
|
|
||||||
.map(|t| match t {
|
|
||||||
Token::Literal(s) => format!("L({s})"),
|
|
||||||
Token::BootTime => "B".to_string(),
|
|
||||||
Token::WallClock => "W".to_string(),
|
|
||||||
Token::Pid => "P".to_string(),
|
|
||||||
Token::Tid => "I".to_string(),
|
|
||||||
Token::Thread => "T".to_string(),
|
|
||||||
Token::Level => "V".to_string(),
|
|
||||||
Token::Location => "O".to_string(),
|
|
||||||
Token::Msg => "M".to_string(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("|")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_plain_literal() {
|
|
||||||
let tokens = parse_format("hello world").unwrap();
|
|
||||||
assert_eq!(render(&tokens), "L(hello world)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_empty_string() {
|
|
||||||
let tokens = parse_format("").unwrap();
|
|
||||||
assert!(tokens.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_all_known_tokens() {
|
|
||||||
let tokens = parse_format(
|
|
||||||
"[{boottime}] {wallclock} {pid}/{tid} <{thread}> {level} {location} -- {msg}",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
render(&tokens),
|
|
||||||
"L([)|B|L(] )|W|L( )|P|L(/)|I|L( <)|T|L(> )|V|L( )|O|L( -- )|M"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_default_format_succeeds() {
|
|
||||||
let tokens = parse_format(DEFAULT_FORMAT).unwrap();
|
|
||||||
// Default format has 5 tokens interleaved with literals.
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t, Token::BootTime)));
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t, Token::Thread)));
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t, Token::Level)));
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t, Token::Location)));
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t, Token::Msg)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_escaped_braces() {
|
|
||||||
let tokens = parse_format("{{not-a-token}}").unwrap();
|
|
||||||
assert_eq!(render(&tokens), "L({not-a-token})");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_escaped_braces_around_token() {
|
|
||||||
let tokens = parse_format("{{{level}}}").unwrap();
|
|
||||||
assert_eq!(render(&tokens), "L({)|V|L(})");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_unterminated_brace_errors() {
|
|
||||||
match parse_format("hello {level") {
|
|
||||||
Err(Error::UnterminatedBrace) => {}
|
|
||||||
Err(other) => panic!("unexpected error: {other:?}"),
|
|
||||||
Ok(_) => panic!("expected error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_unmatched_close_brace_errors() {
|
|
||||||
match parse_format("hello }") {
|
|
||||||
Err(Error::UnmatchedBrace) => {}
|
|
||||||
Err(other) => panic!("unexpected error: {other:?}"),
|
|
||||||
Ok(_) => panic!("expected error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_unknown_token_errors() {
|
|
||||||
match parse_format("{nope}") {
|
|
||||||
Err(Error::UnknownToken(name)) => assert_eq!(name, "nope"),
|
|
||||||
Err(other) => panic!("unexpected error: {other:?}"),
|
|
||||||
Ok(_) => panic!("expected error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_new_uses_default_format() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap();
|
|
||||||
// The default format has all 5 dynamic tokens.
|
|
||||||
assert_eq!(
|
|
||||||
logger
|
|
||||||
.tokens
|
|
||||||
.iter()
|
|
||||||
.filter(|t| !matches!(t, Token::Literal(_)))
|
|
||||||
.count(),
|
|
||||||
5
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_enabled_always_true() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf), DEFAULT_FORMAT).unwrap();
|
|
||||||
let metadata = log::Metadata::builder()
|
|
||||||
.level(log::Level::Trace)
|
|
||||||
.target("anything")
|
|
||||||
.build();
|
|
||||||
assert!(logger.enabled(&metadata));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_writes_expected_fields() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!("hello {}", "world"))
|
|
||||||
.level(log::Level::Info)
|
|
||||||
.target("unit_test_target")
|
|
||||||
.file(Some("foo.rs"))
|
|
||||||
.line(Some(42))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
assert!(out.starts_with("cloud-hypervisor: "), "got: {out}");
|
|
||||||
assert!(out.contains("INFO"), "got: {out}");
|
|
||||||
assert!(out.contains("foo.rs:42"), "got: {out}");
|
|
||||||
assert!(out.contains("hello world"), "got: {out}");
|
|
||||||
assert!(out.ends_with("\r\n"), "got: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_uses_target_when_no_file() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!("no location"))
|
|
||||||
.level(log::Level::Warn)
|
|
||||||
.target("my_target")
|
|
||||||
.file(None)
|
|
||||||
.line(None)
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
assert!(out.contains("my_target"), "got: {out}");
|
|
||||||
assert!(!out.contains("foo.rs"), "got: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_wallclock_is_rfc3339() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), "{wallclock}").unwrap();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!(""))
|
|
||||||
.level(log::Level::Info)
|
|
||||||
.target("t")
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
let out = out.trim();
|
|
||||||
assert_eq!(out.len(), 27, "got: {out}");
|
|
||||||
assert_eq!(&out[4..5], "-", "got: {out}");
|
|
||||||
assert_eq!(&out[7..8], "-", "got: {out}");
|
|
||||||
assert_eq!(&out[10..11], "T", "got: {out}");
|
|
||||||
assert_eq!(&out[13..14], ":", "got: {out}");
|
|
||||||
assert_eq!(&out[16..17], ":", "got: {out}");
|
|
||||||
assert_eq!(&out[19..20], ".", "got: {out}");
|
|
||||||
assert!(out.ends_with('Z'), "got: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_pid_token() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), "{pid}").unwrap();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!(""))
|
|
||||||
.level(log::Level::Info)
|
|
||||||
.target("t")
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
let out = out.trim();
|
|
||||||
assert_eq!(out, std::process::id().to_string(), "got: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_tid_token() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), "{tid}").unwrap();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!(""))
|
|
||||||
.level(log::Level::Info)
|
|
||||||
.target("t")
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
let out = out.trim();
|
|
||||||
let tid: i64 = out.parse().expect("tid should be numeric");
|
|
||||||
assert!(tid > 0, "got: {tid}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logger_appends_each_record() {
|
|
||||||
let buf = SharedBuffer::default();
|
|
||||||
let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap();
|
|
||||||
|
|
||||||
for i in 0..3 {
|
|
||||||
logger.log(
|
|
||||||
&log::Record::builder()
|
|
||||||
.args(format_args!("entry-{i}"))
|
|
||||||
.level(log::Level::Debug)
|
|
||||||
.target("t")
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let out = buf.contents();
|
|
||||||
assert_eq!(out.matches("entry-").count(), 3, "got: {out}");
|
|
||||||
assert_eq!(out.matches("\r\n").count(), 3, "got: {out}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
//
|
//
|
||||||
|
|
||||||
mod logger;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_util;
|
mod test_util;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
use std::{env, io};
|
use std::{env, io};
|
||||||
|
|
||||||
@@ -32,16 +32,13 @@ use vmm::vm_config::FwCfgConfig;
|
|||||||
#[cfg(feature = "ivshmem")]
|
#[cfg(feature = "ivshmem")]
|
||||||
use vmm::vm_config::IvshmemConfig;
|
use vmm::vm_config::IvshmemConfig;
|
||||||
use vmm::vm_config::{
|
use vmm::vm_config::{
|
||||||
BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig,
|
BalloonConfig, DeviceConfig, DiskConfig, FsConfig, LandlockConfig, NetConfig, NumaConfig,
|
||||||
LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig,
|
PciSegmentConfig, PmemConfig, RateLimiterGroupConfig, TpmConfig, UserDeviceConfig, VdpaConfig,
|
||||||
RateLimiterGroupConfig, RngConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig,
|
|
||||||
VmConfig, VsockConfig,
|
VmConfig, VsockConfig,
|
||||||
};
|
};
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
use vmm_sys_util::signal::block_signal;
|
use vmm_sys_util::signal::block_signal;
|
||||||
|
|
||||||
use crate::logger::Logger;
|
|
||||||
|
|
||||||
#[cfg(feature = "dhat-heap")]
|
#[cfg(feature = "dhat-heap")]
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: dhat::Alloc = dhat::Alloc;
|
static ALLOC: dhat::Alloc = dhat::Alloc;
|
||||||
@@ -97,8 +94,6 @@ enum Error {
|
|||||||
BareGdb,
|
BareGdb,
|
||||||
#[error("Error creating log file")]
|
#[error("Error creating log file")]
|
||||||
LogFileCreation(#[source] std::io::Error),
|
LogFileCreation(#[source] std::io::Error),
|
||||||
#[error("Error parsing logger format")]
|
|
||||||
LoggerFormat(#[source] logger::Error),
|
|
||||||
#[error("Error setting up logger")]
|
#[error("Error setting up logger")]
|
||||||
LoggerSetup(#[source] log::SetLoggerError),
|
LoggerSetup(#[source] log::SetLoggerError),
|
||||||
#[error("Failed to gracefully shutdown http api")]
|
#[error("Failed to gracefully shutdown http api")]
|
||||||
@@ -121,6 +116,47 @@ enum FdTableError {
|
|||||||
Dup2(#[source] std::io::Error),
|
Dup2(#[source] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Logger {
|
||||||
|
output: Mutex<Box<dyn std::io::Write + Send>>,
|
||||||
|
start: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl log::Log for Logger {
|
||||||
|
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &log::Record) {
|
||||||
|
if !self.enabled(record.metadata()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
let duration = now.duration_since(self.start);
|
||||||
|
let duration_s = duration.as_secs_f32();
|
||||||
|
|
||||||
|
let location = if let (Some(file), Some(line)) = (record.file(), record.line()) {
|
||||||
|
format!("{file}:{line}")
|
||||||
|
} else {
|
||||||
|
record.target().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut out = self.output.lock().unwrap();
|
||||||
|
write!(
|
||||||
|
&mut *out,
|
||||||
|
// 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned
|
||||||
|
"cloud-hypervisor: {:>10.6?}s: <{}> {}:{} -- {}\r\n",
|
||||||
|
duration_s,
|
||||||
|
std::thread::current().name().unwrap_or("anonymous"),
|
||||||
|
record.level(),
|
||||||
|
location,
|
||||||
|
record.args(),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_default_values() -> (String, String, String) {
|
fn prepare_default_values() -> (String, String, String) {
|
||||||
(default_vcpus(), default_memory(), default_rng())
|
(default_vcpus(), default_memory(), default_rng())
|
||||||
}
|
}
|
||||||
@@ -138,7 +174,7 @@ fn default_memory() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_rng() -> String {
|
fn default_rng() -> String {
|
||||||
format!("src={}", RngConfig::DEFAULT_RNG_SOURCE)
|
format!("src={}", vm_config::DEFAULT_RNG_SOURCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all [`Arg`]s in alphabetical order. This is the order used in the
|
/// Returns all [`Arg`]s in alphabetical order. This is the order used in the
|
||||||
@@ -151,7 +187,7 @@ fn get_cli_options_sorted(
|
|||||||
[
|
[
|
||||||
Arg::new("api-socket")
|
Arg::new("api-socket")
|
||||||
.long("api-socket")
|
.long("api-socket")
|
||||||
.help("HTTP API socket (UNIX domain socket): path=<path> or fd=<fd>.")
|
.help("HTTP API socket (UNIX domain socket): path=</path/to/a/file> or fd=<fd>.")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
Arg::new("balloon")
|
Arg::new("balloon")
|
||||||
@@ -163,10 +199,11 @@ fn get_cli_options_sorted(
|
|||||||
.long("cmdline")
|
.long("cmdline")
|
||||||
.help("Kernel command line")
|
.help("Kernel command line")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vm-config"),
|
.group("vm-config"), Arg::new("console")
|
||||||
Arg::new("console")
|
|
||||||
.long("console")
|
.long("console")
|
||||||
.help(ConsoleConfig::SYNTAX)
|
.help(
|
||||||
|
"Control (virtio) console: \"off|null|pty|tty|file=</path/to/a/file>,iommu=on|off\"",
|
||||||
|
)
|
||||||
.default_value("tty")
|
.default_value("tty")
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("cpus")
|
Arg::new("cpus")
|
||||||
@@ -177,16 +214,16 @@ fn get_cli_options_sorted(
|
|||||||
kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,\
|
kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,\
|
||||||
affinity=<list_of_vcpus_with_their_associated_cpuset>,\
|
affinity=<list_of_vcpus_with_their_associated_cpuset>,\
|
||||||
features=<list_of_features_to_enable>,\
|
features=<list_of_features_to_enable>,\
|
||||||
nested=on|off,core_scheduling=vm|vcpu|off",
|
nested=on|off",
|
||||||
)
|
)
|
||||||
.default_value(default_vcpus)
|
.default_value(default_vcpus)
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
#[cfg(feature = "dbus_api")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
Arg::new("dbus-object-path")
|
Arg::new("debug-console")
|
||||||
.long("dbus-object-path")
|
.long("debug-console")
|
||||||
.help("Object path to serve the dbus interface")
|
.help("Debug console: off|pty|tty|file=</path/to/a/file>,iobase=<port in hex>")
|
||||||
.num_args(1)
|
.default_value("off,iobase=0xe9")
|
||||||
.group("vmm-config"),
|
.group("vm-config"),
|
||||||
#[cfg(feature = "dbus_api")]
|
#[cfg(feature = "dbus_api")]
|
||||||
Arg::new("dbus-service-name")
|
Arg::new("dbus-service-name")
|
||||||
.long("dbus-service-name")
|
.long("dbus-service-name")
|
||||||
@@ -194,33 +231,31 @@ fn get_cli_options_sorted(
|
|||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
#[cfg(feature = "dbus_api")]
|
#[cfg(feature = "dbus_api")]
|
||||||
|
Arg::new("dbus-object-path")
|
||||||
|
.long("dbus-object-path")
|
||||||
|
.help("Object path to serve the dbus interface")
|
||||||
|
.num_args(1)
|
||||||
|
.group("vmm-config"),
|
||||||
|
#[cfg(feature = "dbus_api")]
|
||||||
Arg::new("dbus-system-bus")
|
Arg::new("dbus-system-bus")
|
||||||
.long("dbus-system-bus")
|
.long("dbus-system-bus")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Use the system bus instead of a session bus")
|
.help("Use the system bus instead of a session bus")
|
||||||
.num_args(0)
|
.num_args(0)
|
||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
#[cfg(target_arch = "x86_64")]
|
|
||||||
Arg::new("debug-console")
|
|
||||||
.long("debug-console")
|
|
||||||
.help("Debug console: off|pty|tty|file=<path>,iobase=<port in hex>")
|
|
||||||
.default_value("off,iobase=0xe9")
|
|
||||||
.group("vm-config"),
|
|
||||||
Arg::new("device")
|
Arg::new("device")
|
||||||
.long("device")
|
.long("device")
|
||||||
.help(DeviceConfig::SYNTAX)
|
.help(DeviceConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("disk")
|
Arg::new("disk")
|
||||||
.long("disk")
|
.long("disk")
|
||||||
.help(DiskConfig::SYNTAX)
|
.help(DiskConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("event-monitor")
|
Arg::new("event-monitor")
|
||||||
.long("event-monitor")
|
.long("event-monitor")
|
||||||
.help("Path to report events on: path=<path> or fd=<fd>")
|
.help("File to report events on: path=</path/to/a/file> or fd=<fd>")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
Arg::new("firmware")
|
Arg::new("firmware")
|
||||||
@@ -232,7 +267,6 @@ fn get_cli_options_sorted(
|
|||||||
.long("fs")
|
.long("fs")
|
||||||
.help(FsConfig::SYNTAX)
|
.help(FsConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
#[cfg(feature = "fw_cfg")]
|
#[cfg(feature = "fw_cfg")]
|
||||||
Arg::new("fw-cfg-config")
|
Arg::new("fw-cfg-config")
|
||||||
@@ -243,15 +277,9 @@ fn get_cli_options_sorted(
|
|||||||
#[cfg(feature = "guest_debug")]
|
#[cfg(feature = "guest_debug")]
|
||||||
Arg::new("gdb")
|
Arg::new("gdb")
|
||||||
.long("gdb")
|
.long("gdb")
|
||||||
.help("GDB socket (UNIX domain socket): path=<path>")
|
.help("GDB socket (UNIX domain socket): path=</path/to/a/file>")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
Arg::new("generic-vhost-user")
|
|
||||||
.long("generic-vhost-user")
|
|
||||||
.help(GenericVhostUserConfig::SYNTAX)
|
|
||||||
.num_args(1..)
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
|
||||||
#[cfg(feature = "igvm")]
|
#[cfg(feature = "igvm")]
|
||||||
Arg::new("igvm")
|
Arg::new("igvm")
|
||||||
.long("igvm")
|
.long("igvm")
|
||||||
@@ -286,7 +314,9 @@ fn get_cli_options_sorted(
|
|||||||
Arg::new("landlock")
|
Arg::new("landlock")
|
||||||
.long("landlock")
|
.long("landlock")
|
||||||
.num_args(0)
|
.num_args(0)
|
||||||
.help("enable/disable Landlock.")
|
.help(
|
||||||
|
"enable/disable Landlock.",
|
||||||
|
)
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.default_value("false")
|
.default_value("false")
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
@@ -294,19 +324,12 @@ fn get_cli_options_sorted(
|
|||||||
.long("landlock-rules")
|
.long("landlock-rules")
|
||||||
.help(LandlockConfig::SYNTAX)
|
.help(LandlockConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("log-file")
|
Arg::new("log-file")
|
||||||
.long("log-file")
|
.long("log-file")
|
||||||
.help("Log file. Standard error is used if not specified")
|
.help("Log file. Standard error is used if not specified")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("logging"),
|
.group("logging"),
|
||||||
Arg::new("log-format")
|
|
||||||
.long("log-format")
|
|
||||||
.help("Log format. Available tokens: {boottime}, {wallclock}, {pid}, {tid}, {thread}, {level}, {location}, {msg}")
|
|
||||||
.num_args(1)
|
|
||||||
.default_value(logger::DEFAULT_FORMAT)
|
|
||||||
.group("logging"),
|
|
||||||
Arg::new("memory")
|
Arg::new("memory")
|
||||||
.long("memory")
|
.long("memory")
|
||||||
.help(
|
.help(
|
||||||
@@ -333,42 +356,33 @@ fn get_cli_options_sorted(
|
|||||||
prefault=on|off\"",
|
prefault=on|off\"",
|
||||||
)
|
)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("net")
|
Arg::new("net")
|
||||||
.long("net")
|
.long("net")
|
||||||
.help(NetConfig::SYNTAX)
|
.help(NetConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("no-shutdown")
|
|
||||||
.long("no-shutdown")
|
|
||||||
.help("Do not exit the VMM when the guest shuts down")
|
|
||||||
.num_args(0)
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.group("vmm-config"),
|
|
||||||
Arg::new("numa")
|
Arg::new("numa")
|
||||||
.long("numa")
|
.long("numa")
|
||||||
.help(NumaConfig::SYNTAX)
|
.help(NumaConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("pci-segment")
|
Arg::new("pci-segment")
|
||||||
.long("pci-segment")
|
.long("pci-segment")
|
||||||
.help(PciSegmentConfig::SYNTAX)
|
.help(PciSegmentConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("platform")
|
Arg::new("platform")
|
||||||
.long("platform")
|
.long("platform")
|
||||||
.help(PlatformConfig::syntax())
|
.help(
|
||||||
|
"num_pci_segments=<num_pci_segments>,iommu_segments=<list_of_segments>,iommu_address_width=<bits>,serial_number=<dmi_device_serial_number>,uuid=<dmi_device_uuid>,oem_strings=<list_of_strings>"
|
||||||
|
)
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("pmem")
|
Arg::new("pmem")
|
||||||
.long("pmem")
|
.long("pmem")
|
||||||
.help(PmemConfig::SYNTAX)
|
.help(PmemConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
#[cfg(feature = "pvmemcontrol")]
|
#[cfg(feature = "pvmemcontrol")]
|
||||||
Arg::new("pvmemcontrol")
|
Arg::new("pvmemcontrol")
|
||||||
@@ -387,7 +401,6 @@ fn get_cli_options_sorted(
|
|||||||
.long("rate-limit-group")
|
.long("rate-limit-group")
|
||||||
.help(RateLimiterGroupConfig::SYNTAX)
|
.help(RateLimiterGroupConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("restore")
|
Arg::new("restore")
|
||||||
.long("restore")
|
.long("restore")
|
||||||
@@ -396,7 +409,9 @@ fn get_cli_options_sorted(
|
|||||||
.group("vmm-config"),
|
.group("vmm-config"),
|
||||||
Arg::new("rng")
|
Arg::new("rng")
|
||||||
.long("rng")
|
.long("rng")
|
||||||
.help(RngConfig::SYNTAX)
|
.help(
|
||||||
|
"Random number generator parameters \"src=<entropy_source_path>,iommu=on|off\"",
|
||||||
|
)
|
||||||
.default_value(default_rng)
|
.default_value(default_rng)
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("seccomp")
|
Arg::new("seccomp")
|
||||||
@@ -406,7 +421,7 @@ fn get_cli_options_sorted(
|
|||||||
.default_value("true"),
|
.default_value("true"),
|
||||||
Arg::new("serial")
|
Arg::new("serial")
|
||||||
.long("serial")
|
.long("serial")
|
||||||
.help(SerialConfig::SYNTAX)
|
.help("Control serial port: off|null|pty|tty|file=</path/to/a/file>|socket=</path/to/a/file>")
|
||||||
.default_value("null")
|
.default_value("null")
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("tpm")
|
Arg::new("tpm")
|
||||||
@@ -418,7 +433,6 @@ fn get_cli_options_sorted(
|
|||||||
.long("user-device")
|
.long("user-device")
|
||||||
.help(UserDeviceConfig::SYNTAX)
|
.help(UserDeviceConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("v")
|
Arg::new("v")
|
||||||
.short('v')
|
.short('v')
|
||||||
@@ -429,7 +443,6 @@ fn get_cli_options_sorted(
|
|||||||
.long("vdpa")
|
.long("vdpa")
|
||||||
.help(VdpaConfig::SYNTAX)
|
.help(VdpaConfig::SYNTAX)
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
.action(ArgAction::Append)
|
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
Arg::new("version")
|
Arg::new("version")
|
||||||
.short('V')
|
.short('V')
|
||||||
@@ -448,9 +461,7 @@ fn get_cli_options_sorted(
|
|||||||
.num_args(0)
|
.num_args(0)
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.group("vm-config"),
|
.group("vm-config"),
|
||||||
]
|
].to_vec().into_boxed_slice()
|
||||||
.to_vec()
|
|
||||||
.into_boxed_slice()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the CLI definition of Cloud Hypervisor.
|
/// Creates the CLI definition of Cloud Hypervisor.
|
||||||
@@ -476,37 +487,7 @@ fn create_app(default_vcpus: String, default_memory: String, default_rng: String
|
|||||||
.args(args)
|
.args(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_api_socket(cmd_arguments: &ArgMatches) -> Result<(Option<String>, Option<RawFd>), Error> {
|
fn start_vmm(cmd_arguments: &ArgMatches) -> Result<Option<String>, Error> {
|
||||||
if let Some(socket_config) = cmd_arguments.get_one::<String>("api-socket") {
|
|
||||||
let mut parser = OptionParser::new();
|
|
||||||
parser.add("path").add("fd");
|
|
||||||
parser.parse(socket_config).unwrap_or_default();
|
|
||||||
|
|
||||||
if let Some(fd) = parser.get("fd") {
|
|
||||||
Ok((
|
|
||||||
None,
|
|
||||||
Some(fd.parse::<RawFd>().map_err(Error::ParsingApiSocket)?),
|
|
||||||
))
|
|
||||||
} else if let Some(path) = parser.get("path") {
|
|
||||||
Ok((Some(path), None))
|
|
||||||
} else {
|
|
||||||
Ok((
|
|
||||||
cmd_arguments
|
|
||||||
.get_one::<String>("api-socket")
|
|
||||||
.map(|s| s.to_string()),
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok((None, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_vmm(
|
|
||||||
cmd_arguments: &ArgMatches,
|
|
||||||
api_socket_path: &Option<String>,
|
|
||||||
api_socket_fd: Option<RawFd>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let log_level = match cmd_arguments.get_count("v") {
|
let log_level = match cmd_arguments.get_count("v") {
|
||||||
0 => LevelFilter::Warn,
|
0 => LevelFilter::Warn,
|
||||||
1 => LevelFilter::Info,
|
1 => LevelFilter::Info,
|
||||||
@@ -522,11 +503,37 @@ fn start_vmm(
|
|||||||
Box::new(std::io::stderr())
|
Box::new(std::io::stderr())
|
||||||
};
|
};
|
||||||
|
|
||||||
let format = cmd_arguments.get_one::<String>("log-format").unwrap();
|
log::set_boxed_logger(Box::new(Logger {
|
||||||
let logger = Logger::new(log_file, format).map_err(Error::LoggerFormat)?;
|
output: Mutex::new(log_file),
|
||||||
log::set_boxed_logger(Box::new(logger))
|
start: std::time::Instant::now(),
|
||||||
.map(|()| log::set_max_level(log_level))
|
}))
|
||||||
.map_err(Error::LoggerSetup)?;
|
.map(|()| log::set_max_level(log_level))
|
||||||
|
.map_err(Error::LoggerSetup)?;
|
||||||
|
|
||||||
|
let (api_socket_path, api_socket_fd) =
|
||||||
|
if let Some(socket_config) = cmd_arguments.get_one::<String>("api-socket") {
|
||||||
|
let mut parser = OptionParser::new();
|
||||||
|
parser.add("path").add("fd");
|
||||||
|
parser.parse(socket_config).unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(fd) = parser.get("fd") {
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
Some(fd.parse::<RawFd>().map_err(Error::ParsingApiSocket)?),
|
||||||
|
)
|
||||||
|
} else if let Some(path) = parser.get("path") {
|
||||||
|
(Some(path), None)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
cmd_arguments
|
||||||
|
.get_one::<String>("api-socket")
|
||||||
|
.map(|s| s.to_string()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
let (api_request_sender, api_request_receiver) = channel();
|
let (api_request_sender, api_request_receiver) = channel();
|
||||||
let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?;
|
let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?;
|
||||||
@@ -583,8 +590,6 @@ fn start_vmm(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("{} starting", env!("BUILD_VERSION"));
|
|
||||||
|
|
||||||
let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?;
|
let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?;
|
||||||
|
|
||||||
#[cfg(feature = "guest_debug")]
|
#[cfg(feature = "guest_debug")]
|
||||||
@@ -608,7 +613,6 @@ fn start_vmm(
|
|||||||
|
|
||||||
let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?;
|
let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?;
|
||||||
let landlock_enable = cmd_arguments.get_flag("landlock");
|
let landlock_enable = cmd_arguments.get_flag("landlock");
|
||||||
let no_shutdown = cmd_arguments.get_flag("no-shutdown");
|
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut event_monitor = cmd_arguments
|
let mut event_monitor = cmd_arguments
|
||||||
@@ -689,7 +693,7 @@ fn start_vmm(
|
|||||||
|
|
||||||
let vmm_thread_handle = vmm::start_vmm_thread(
|
let vmm_thread_handle = vmm::start_vmm_thread(
|
||||||
vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")),
|
vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")),
|
||||||
api_socket_path,
|
&api_socket_path,
|
||||||
api_socket_fd,
|
api_socket_fd,
|
||||||
#[cfg(feature = "dbus_api")]
|
#[cfg(feature = "dbus_api")]
|
||||||
dbus_options,
|
dbus_options,
|
||||||
@@ -705,7 +709,6 @@ fn start_vmm(
|
|||||||
exit_evt.try_clone().unwrap(),
|
exit_evt.try_clone().unwrap(),
|
||||||
&seccomp_action,
|
&seccomp_action,
|
||||||
hypervisor,
|
hypervisor,
|
||||||
no_shutdown,
|
|
||||||
landlock_enable,
|
landlock_enable,
|
||||||
)
|
)
|
||||||
.map_err(Error::StartVmmThread)?;
|
.map_err(Error::StartVmmThread)?;
|
||||||
@@ -776,7 +779,7 @@ fn start_vmm(
|
|||||||
dbus_api_graceful_shutdown(chs);
|
dbus_api_graceful_shutdown(chs);
|
||||||
}
|
}
|
||||||
|
|
||||||
r
|
r.map(|_| api_socket_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a best-effort solution to the latency induced by the RCU
|
// This is a best-effort solution to the latency induced by the RCU
|
||||||
@@ -882,22 +885,9 @@ fn main() {
|
|||||||
warn!("Error expanding FD table: {e}");
|
warn!("Error expanding FD table: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (api_socket_path, api_socket_fd) = match parse_api_socket(&cmd_arguments) {
|
let exit_code = match start_vmm(&cmd_arguments) {
|
||||||
Ok(p) => p,
|
Ok(path) => {
|
||||||
Err(top_error) => {
|
path.map(|s| std::fs::remove_file(s).ok());
|
||||||
cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor", |_, _, _| None);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let vmm_result = start_vmm(&cmd_arguments, &api_socket_path, api_socket_fd);
|
|
||||||
|
|
||||||
if let Some(ref p) = api_socket_path {
|
|
||||||
let _ = std::fs::remove_file(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
let exit_code = match vmm_result {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Cloud Hypervisor exited successfully");
|
info!("Cloud Hypervisor exited successfully");
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -921,9 +911,8 @@ mod unit_tests {
|
|||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
use vmm::vm_config::DebugConsoleConfig;
|
use vmm::vm_config::DebugConsoleConfig;
|
||||||
use vmm::vm_config::{
|
use vmm::vm_config::{
|
||||||
CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures,
|
ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig,
|
||||||
CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig,
|
PayloadConfig, RngConfig, VmConfig,
|
||||||
SerialConfig, VmConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::test_util::assert_args_sorted;
|
use crate::test_util::assert_args_sorted;
|
||||||
@@ -974,7 +963,6 @@ mod unit_tests {
|
|||||||
affinity: None,
|
affinity: None,
|
||||||
features: CpuFeatures::default(),
|
features: CpuFeatures::default(),
|
||||||
nested: true,
|
nested: true,
|
||||||
core_scheduling: CoreScheduling::Vm,
|
|
||||||
},
|
},
|
||||||
memory: MemoryConfig {
|
memory: MemoryConfig {
|
||||||
size: 536_870_912,
|
size: 536_870_912,
|
||||||
@@ -1006,26 +994,22 @@ mod unit_tests {
|
|||||||
net: None,
|
net: None,
|
||||||
rng: RngConfig {
|
rng: RngConfig {
|
||||||
src: PathBuf::from("/dev/urandom"),
|
src: PathBuf::from("/dev/urandom"),
|
||||||
pci_common: PciDeviceCommonConfig::default(),
|
iommu: false,
|
||||||
},
|
},
|
||||||
balloon: None,
|
balloon: None,
|
||||||
fs: None,
|
fs: None,
|
||||||
generic_vhost_user: None,
|
|
||||||
pmem: None,
|
pmem: None,
|
||||||
serial: SerialConfig {
|
serial: ConsoleConfig {
|
||||||
common: CommonConsoleConfig {
|
file: None,
|
||||||
file: None,
|
mode: ConsoleOutputMode::Null,
|
||||||
mode: ConsoleOutputMode::Null,
|
iommu: false,
|
||||||
socket: None,
|
socket: None,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
console: ConsoleConfig {
|
console: ConsoleConfig {
|
||||||
common: CommonConsoleConfig {
|
file: None,
|
||||||
file: None,
|
mode: ConsoleOutputMode::Tty,
|
||||||
mode: ConsoleOutputMode::Tty,
|
iommu: false,
|
||||||
socket: None,
|
socket: None,
|
||||||
},
|
|
||||||
pci_common: PciDeviceCommonConfig::default(),
|
|
||||||
},
|
},
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
debug_console: DebugConsoleConfig::default(),
|
debug_console: DebugConsoleConfig::default(),
|
||||||
@@ -1693,12 +1677,10 @@ mod unit_tests {
|
|||||||
"--serial",
|
"--serial",
|
||||||
"null",
|
"null",
|
||||||
"--console",
|
"--console",
|
||||||
"tty,pci_segment=1,pci_device_id=7",
|
"tty",
|
||||||
],
|
],
|
||||||
r#"{
|
r#"{
|
||||||
"payload": {"kernel": "/path/to/kernel"},
|
"payload": {"kernel": "/path/to/kernel"}
|
||||||
"serial": {"mode": "Null"},
|
|
||||||
"console": {"mode": "Tty", "iommu": false, "pci_segment": 1, "pci_device_id": 7}
|
|
||||||
}"#,
|
}"#,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
pub(crate) mod tests_wrappers;
|
|
||||||
pub(crate) mod utils;
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,327 +0,0 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
//
|
|
||||||
#![cfg(any(devcli_testenv, clippy))]
|
|
||||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
|
||||||
// When enabling the `mshv` feature, we skip quite some tests and
|
|
||||||
// hence have known dead-code. This annotation silences dead-code
|
|
||||||
// related warnings for our quality workflow to pass.
|
|
||||||
#![allow(dead_code)]
|
|
||||||
mod common;
|
|
||||||
|
|
||||||
#[cfg(all(feature = "sev_snp", target_arch = "x86_64"))]
|
|
||||||
mod common_cvm {
|
|
||||||
use block::ImageType;
|
|
||||||
use common::tests_wrappers::*;
|
|
||||||
use common::utils::*;
|
|
||||||
use test_infra::*;
|
|
||||||
const NUM_PCI_SEGMENTS: u16 = 8;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
macro_rules! basic_cvm_guest {
|
|
||||||
($image_name:expr) => {{
|
|
||||||
let disk_config = UbuntuDiskConfig::new($image_name.to_string());
|
|
||||||
GuestFactory::new_confidential_guest_factory().create_guest(Box::new(disk_config))
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_jammy_simple_launch() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
|
|
||||||
_test_simple_launch(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_api_http_create_boot() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
|
|
||||||
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
|
|
||||||
_test_api_create_boot(&target_api, &guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_api_http_shutdown() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
|
|
||||||
|
|
||||||
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
|
|
||||||
_test_api_shutdown(&target_api, &guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_api_http_delete() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
|
|
||||||
_test_api_delete(&target_api, &guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_power_button() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_power_button(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_vsock() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_virtio_vsock(&guest, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multi_cpu() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_multi_cpu(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cpu_affinity() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
|
|
||||||
_test_cpu_affinity(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_queue_affinity() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
|
|
||||||
_test_virtio_queue_affinity(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pci_msi() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_pci_msi(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_net_ctrl_queue() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_virtio_net_ctrl_queue(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pci_multiple_segments() {
|
|
||||||
// Use 8 segments to test the multiple segment support since it's more than the default 6
|
|
||||||
// supported by Linux
|
|
||||||
// IGVM file used by Sev-Snp Guest now support up to 8 segments, so we can use 8 segments for testing.
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_pci_multiple_segments(&guest, NUM_PCI_SEGMENTS, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_direct_kernel_boot() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_direct_kernel_boot(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_io_uring() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, false, true, false, false, ImageType::Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_aio() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, true, false, false, false, ImageType::Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_sync() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, true, true, false, false, ImageType::Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2_zlib() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2_ZLIB,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2_zstd() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2_ZSTD,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2_backing_zstd_file() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE,
|
|
||||||
);
|
|
||||||
|
|
||||||
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2_backing_uncompressed_file() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE,
|
|
||||||
);
|
|
||||||
|
|
||||||
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_qcow2_backing_raw_file() {
|
|
||||||
let guest = make_virtio_block_guest(
|
|
||||||
&GuestFactory::new_confidential_guest_factory(),
|
|
||||||
JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE,
|
|
||||||
);
|
|
||||||
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_block_dynamic_vhdx_expand() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_virtio_block_dynamic_vhdx_expand(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_irqchip() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_split_irqchip(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dmi_uuid() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_dmi_uuid(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dmi_oem_strings() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_dmi_oem_strings(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_network_interfaces() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_multiple_network_interfaces(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serial_off() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_serial_off(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_virtio_console() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_virtio_console(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_console_file() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_console_file(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_direct_kernel_boot_noacpi() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_direct_kernel_boot_noacpi(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pci_bar_reprogramming() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_pci_bar_reprogramming(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_memory_overhead() {
|
|
||||||
let guest_memory_size_kb: u32 = 512 * 1024;
|
|
||||||
let guest =
|
|
||||||
basic_cvm_guest!(JAMMY_IMAGE_NAME).with_memory(&format!("{guest_memory_size_kb}K"));
|
|
||||||
_test_memory_overhead(&guest, guest_memory_size_kb);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_landlock() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_landlock(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_disk_hotplug() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_disk_hotplug(&guest, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_net_hotplug() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_net_hotplug(&guest, NUM_PCI_SEGMENTS, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_counters() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_counters(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_watchdog() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_watchdog(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pvpanic() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME);
|
|
||||||
_test_pvpanic(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tap_from_fd() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
|
|
||||||
_test_tap_from_fd(&guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_macvtap() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
|
|
||||||
_test_macvtap(&guest, false, "guestmacvtap0", "hostmacvtap0");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_macvtap_hotplug() {
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
|
|
||||||
_test_macvtap(&guest, true, "guestmacvtap1", "hostmacvtap1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vdpa_block() {
|
|
||||||
assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success());
|
|
||||||
|
|
||||||
let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
|
|
||||||
_test_vdpa_block(&guest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,13 @@
|
|||||||
authors = ["The Chromium OS Authors"]
|
authors = ["The Chromium OS Authors"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
name = "devices"
|
name = "devices"
|
||||||
rust-version.workspace = true
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acpi_tables = { workspace = true }
|
acpi_tables = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
arch = { path = "../arch" }
|
arch = { path = "../arch" }
|
||||||
bitfield-struct = { version = "0.13.0", optional = true }
|
bitfield-struct = { version = "0.12.0", optional = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
byteorder = { workspace = true }
|
byteorder = { workspace = true }
|
||||||
event_monitor = { path = "../event_monitor" }
|
event_monitor = { path = "../event_monitor" }
|
||||||
@@ -21,7 +20,7 @@ linux-loader = { workspace = true, features = [
|
|||||||
"pe",
|
"pe",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
num_enum = "0.7.6"
|
num_enum = "0.7.5"
|
||||||
pci = { path = "../pci" }
|
pci = { path = "../pci" }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
@@ -35,7 +34,7 @@ vm-memory = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
vm-migration = { path = "../vm-migration" }
|
vm-migration = { path = "../vm-migration" }
|
||||||
vmm-sys-util = { workspace = true }
|
vmm-sys-util = { workspace = true }
|
||||||
zerocopy = { version = "0.8.48", features = [
|
zerocopy = { version = "0.8.31", features = [
|
||||||
"alloc",
|
"alloc",
|
||||||
"derive",
|
"derive",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub const GED_DEVICE_ACPI_SIZE: usize = 0x1;
|
|||||||
|
|
||||||
/// A device for handling ACPI shutdown and reboot
|
/// A device for handling ACPI shutdown and reboot
|
||||||
pub struct AcpiShutdownDevice {
|
pub struct AcpiShutdownDevice {
|
||||||
guest_exit_evt: EventFd,
|
exit_evt: EventFd,
|
||||||
reset_evt: EventFd,
|
reset_evt: EventFd,
|
||||||
vcpus_kill_signalled: Arc<AtomicBool>,
|
vcpus_kill_signalled: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
@@ -29,12 +29,12 @@ pub struct AcpiShutdownDevice {
|
|||||||
impl AcpiShutdownDevice {
|
impl AcpiShutdownDevice {
|
||||||
/// Constructs a device that will signal the given event when the guest requests it.
|
/// Constructs a device that will signal the given event when the guest requests it.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
guest_exit_evt: EventFd,
|
exit_evt: EventFd,
|
||||||
reset_evt: EventFd,
|
reset_evt: EventFd,
|
||||||
vcpus_kill_signalled: Arc<AtomicBool>,
|
vcpus_kill_signalled: Arc<AtomicBool>,
|
||||||
) -> AcpiShutdownDevice {
|
) -> AcpiShutdownDevice {
|
||||||
AcpiShutdownDevice {
|
AcpiShutdownDevice {
|
||||||
guest_exit_evt,
|
exit_evt,
|
||||||
reset_evt,
|
reset_evt,
|
||||||
vcpus_kill_signalled,
|
vcpus_kill_signalled,
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ impl BusDevice for AcpiShutdownDevice {
|
|||||||
const SLEEP_VALUE_BIT: u8 = 2;
|
const SLEEP_VALUE_BIT: u8 = 2;
|
||||||
if data[0] == (S5_SLEEP_VALUE << SLEEP_VALUE_BIT) | (1 << SLEEP_STATUS_EN_BIT) {
|
if data[0] == (S5_SLEEP_VALUE << SLEEP_VALUE_BIT) | (1 << SLEEP_STATUS_EN_BIT) {
|
||||||
info!("ACPI Shutdown signalled");
|
info!("ACPI Shutdown signalled");
|
||||||
if let Err(e) = self.guest_exit_evt.write(1) {
|
if let Err(e) = self.exit_evt.write(1) {
|
||||||
error!("Error triggering ACPI shutdown event: {e}");
|
error!("Error triggering ACPI shutdown event: {e}");
|
||||||
}
|
}
|
||||||
// Spin until we are sure the reset_evt has been handled and that when
|
// Spin until we are sure the reset_evt has been handled and that when
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ impl BusDevice for IvshmemDevice {
|
|||||||
impl PciDevice for IvshmemDevice {
|
impl PciDevice for IvshmemDevice {
|
||||||
fn allocate_bars(
|
fn allocate_bars(
|
||||||
&mut self,
|
&mut self,
|
||||||
_allocator: &mut SystemAllocator,
|
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||||
mmio32_allocator: &mut AddressAllocator,
|
mmio32_allocator: &mut AddressAllocator,
|
||||||
mmio64_allocator: &mut AddressAllocator,
|
mmio64_allocator: &mut AddressAllocator,
|
||||||
resources: Option<Vec<Resource>>,
|
resources: Option<Vec<Resource>>,
|
||||||
@@ -382,10 +382,6 @@ impl PciDevice for IvshmemDevice {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) {
|
|
||||||
self.configuration.restore_bar_addr(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ impl BusDevice for Cmos {
|
|||||||
// the tm and timespec struct because it contains only plain data.
|
// the tm and timespec struct because it contains only plain data.
|
||||||
let update_in_progress = unsafe {
|
let update_in_progress = unsafe {
|
||||||
let mut timespec: timespec = mem::zeroed();
|
let mut timespec: timespec = mem::zeroed();
|
||||||
clock_gettime(CLOCK_REALTIME, &raw mut timespec);
|
clock_gettime(CLOCK_REALTIME, &mut timespec as *mut _);
|
||||||
|
|
||||||
// https://github.com/rust-lang/libc/issues/1848
|
// https://github.com/rust-lang/libc/issues/1848
|
||||||
#[cfg_attr(target_env = "musl", allow(deprecated))]
|
#[cfg_attr(target_env = "musl", allow(deprecated))]
|
||||||
let now: time_t = timespec.tv_sec;
|
let now: time_t = timespec.tv_sec;
|
||||||
let mut tm: tm = mem::zeroed();
|
let mut tm: tm = mem::zeroed();
|
||||||
gmtime_r(&now, &raw mut tm);
|
gmtime_r(&now, &mut tm as *mut _);
|
||||||
|
|
||||||
// The following lines of code are safe but depend on tm being in scope.
|
// The following lines of code are safe but depend on tm being in scope.
|
||||||
seconds = tm.tm_sec;
|
seconds = tm.tm_sec;
|
||||||
|
|||||||
@@ -441,17 +441,12 @@ impl FwCfg {
|
|||||||
initramfs: Option<File>,
|
initramfs: Option<File>,
|
||||||
cmdline: Option<std::ffi::CString>,
|
cmdline: Option<std::ffi::CString>,
|
||||||
fw_cfg_item_list: Option<Vec<FwCfgItem>>,
|
fw_cfg_item_list: Option<Vec<FwCfgItem>>,
|
||||||
#[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(mem_size) = mem_size {
|
if let Some(mem_size) = mem_size {
|
||||||
self.add_e820(mem_size)?;
|
self.add_e820(mem_size)?;
|
||||||
}
|
}
|
||||||
if let Some(kernel) = kernel {
|
if let Some(kernel) = kernel {
|
||||||
self.add_kernel_data(
|
self.add_kernel_data(&kernel)?;
|
||||||
&kernel,
|
|
||||||
#[cfg(target_arch = "x86_64")]
|
|
||||||
kvm_sev_snp_enabled,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
if let Some(cmdline) = cmdline {
|
if let Some(cmdline) = cmdline {
|
||||||
self.add_kernel_cmdline(cmdline);
|
self.add_kernel_cmdline(cmdline);
|
||||||
@@ -636,49 +631,24 @@ impl FwCfg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_kernel_data(
|
pub fn add_kernel_data(&mut self, file: &File) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
file: &File,
|
|
||||||
#[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut buffer = vec![0u8; size_of::<boot_params>()];
|
let mut buffer = vec![0u8; size_of::<boot_params>()];
|
||||||
file.read_exact_at(&mut buffer, 0)?;
|
file.read_exact_at(&mut buffer, 0)?;
|
||||||
let bp = boot_params::from_mut_slice(&mut buffer).unwrap();
|
let bp = boot_params::from_mut_slice(&mut buffer).unwrap();
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
{
|
{
|
||||||
// For SEV-SNP guests on KVM, don't modify the kernel header so the
|
// must set to 4 for backwards compatibility
|
||||||
// bytes sent via fw_cfg match what the VMM hashes for the launch digest.
|
// https://docs.kernel.org/arch/x86/boot.html#the-real-mode-kernel-header
|
||||||
// The guest firmware handles these fields itself.
|
if bp.hdr.setup_sects == 0 {
|
||||||
if !kvm_sev_snp_enabled {
|
bp.hdr.setup_sects = 4;
|
||||||
if bp.hdr.setup_sects == 0 {
|
|
||||||
bp.hdr.setup_sects = 4;
|
|
||||||
}
|
|
||||||
bp.hdr.type_of_loader = 0xff;
|
|
||||||
}
|
}
|
||||||
|
// wildcard boot loader type
|
||||||
|
bp.hdr.type_of_loader = 0xff;
|
||||||
}
|
}
|
||||||
#[cfg(target_arch = "aarch64")]
|
#[cfg(target_arch = "aarch64")]
|
||||||
let kernel_start = bp.text_offset;
|
let kernel_start = bp.text_offset;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
let kernel_start = {
|
let kernel_start = (bp.hdr.setup_sects as usize + 1) * 512;
|
||||||
let sects = if bp.hdr.setup_sects == 0 {
|
|
||||||
4
|
|
||||||
} else {
|
|
||||||
bp.hdr.setup_sects
|
|
||||||
};
|
|
||||||
(sects as usize + 1) * 512
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_arch = "x86_64")]
|
|
||||||
if kernel_start <= buffer.len() {
|
|
||||||
buffer.truncate(kernel_start);
|
|
||||||
} else {
|
|
||||||
buffer.resize(kernel_start, 0);
|
|
||||||
file.read_exact_at(
|
|
||||||
&mut buffer[size_of::<boot_params>()..],
|
|
||||||
size_of::<boot_params>() as u64,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.known_items[FW_CFG_SETUP_SIZE as usize] = FwCfgContent::U32(buffer.len() as u32);
|
self.known_items[FW_CFG_SETUP_SIZE as usize] = FwCfgContent::U32(buffer.len() as u32);
|
||||||
self.known_items[FW_CFG_SETUP_DATA as usize] = FwCfgContent::Bytes(buffer);
|
self.known_items[FW_CFG_SETUP_DATA as usize] = FwCfgContent::Bytes(buffer);
|
||||||
self.known_items[FW_CFG_KERNEL_SIZE as usize] =
|
self.known_items[FW_CFG_KERNEL_SIZE as usize] =
|
||||||
@@ -927,32 +897,6 @@ mod unit_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_string_item() {
|
|
||||||
let gm = GuestMemoryAtomic::new(
|
|
||||||
GuestMemoryMmap::from_ranges(&[(GuestAddress(0), RAM_64BIT_START.0 as usize)]).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut fw_cfg = FwCfg::new(gm);
|
|
||||||
|
|
||||||
// Simulate OVMF X-PciMmio64Mb string item for GPU CC passthrough
|
|
||||||
let item = FwCfgItem {
|
|
||||||
name: "opt/ovmf/X-PciMmio64Mb".to_owned(),
|
|
||||||
content: FwCfgContent::Bytes("262144".as_bytes().to_vec()),
|
|
||||||
};
|
|
||||||
fw_cfg.add_item(item).unwrap();
|
|
||||||
|
|
||||||
let expected = b"262144";
|
|
||||||
let mut data = vec![0u8];
|
|
||||||
|
|
||||||
// Select the first file item (FW_CFG_FILE_FIRST = 0x20)
|
|
||||||
fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_FILE_FIRST as u8, 0]);
|
|
||||||
for &byte in expected.iter() {
|
|
||||||
fw_cfg.read(0, DATA_OFFSET, &mut data);
|
|
||||||
assert_eq!(data[0], byte);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dma() {
|
fn test_dma() {
|
||||||
let code = [
|
let code = [
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::sync::{Arc, Barrier, RwLock};
|
use std::sync::{Arc, Barrier, Mutex, RwLock};
|
||||||
use std::{io, result};
|
use std::{io, result};
|
||||||
|
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
@@ -443,7 +443,7 @@ impl PvmemcontrolBusDevice {
|
|||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
assert!(slice.len() >= range_len);
|
assert!(slice.len() >= range_len);
|
||||||
let res = f(slice.ptr_guard_mut().as_ptr().cast(), slice.len());
|
let res = f(slice.ptr_guard_mut().as_ptr() as _, slice.len());
|
||||||
if res != 0 {
|
if res != 0 {
|
||||||
return Err(Error::LibcFail(io::Error::last_os_error()));
|
return Err(Error::LibcFail(io::Error::last_os_error()));
|
||||||
}
|
}
|
||||||
@@ -712,10 +712,6 @@ impl PciDevice for PvmemcontrolPciDevice {
|
|||||||
self.configuration.read_config_register(reg_idx)
|
self.configuration.read_config_register(reg_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) {
|
|
||||||
self.configuration.restore_bar_addr(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -726,7 +722,7 @@ impl PciDevice for PvmemcontrolPciDevice {
|
|||||||
|
|
||||||
fn allocate_bars(
|
fn allocate_bars(
|
||||||
&mut self,
|
&mut self,
|
||||||
_allocator: &mut SystemAllocator,
|
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||||
mmio32_allocator: &mut AddressAllocator,
|
mmio32_allocator: &mut AddressAllocator,
|
||||||
_mmio64_allocator: &mut AddressAllocator,
|
_mmio64_allocator: &mut AddressAllocator,
|
||||||
resources: Option<Vec<Resource>>,
|
resources: Option<Vec<Resource>>,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::result;
|
use std::result;
|
||||||
use std::sync::{Arc, Barrier};
|
use std::sync::{Arc, Barrier, Mutex};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use event_monitor::event;
|
use event_monitor::event;
|
||||||
@@ -174,7 +174,7 @@ impl PciDevice for PvPanicDevice {
|
|||||||
|
|
||||||
fn allocate_bars(
|
fn allocate_bars(
|
||||||
&mut self,
|
&mut self,
|
||||||
_allocator: &mut SystemAllocator,
|
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||||
mmio32_allocator: &mut AddressAllocator,
|
mmio32_allocator: &mut AddressAllocator,
|
||||||
_mmio64_allocator: &mut AddressAllocator,
|
_mmio64_allocator: &mut AddressAllocator,
|
||||||
resources: Option<Vec<Resource>>,
|
resources: Option<Vec<Resource>>,
|
||||||
@@ -231,10 +231,6 @@ impl PciDevice for PvPanicDevice {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) {
|
|
||||||
self.configuration.restore_bar_addr(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_bar(&mut self, _base: u64, _offset: u64, data: &mut [u8]) {
|
fn read_bar(&mut self, _base: u64, _offset: u64, data: &mut [u8]) {
|
||||||
data[0] = self.events;
|
data[0] = self.events;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### WARNING
|
### WARNING
|
||||||
|
|
||||||
This feature is currently only supported on MSHV.
|
This feature is only currently supported on MSHV.
|
||||||
|
|
||||||
AMD Secure Encrypted Virtualization & Secure Nested Paging (SEV-SNP) is an AMD
|
AMD Secure Encrypted Virtualization & Secure Nested Paging (SEV-SNP) is an AMD
|
||||||
technology designed to add strong memory integrity protection to help prevent
|
technology designed to add strong memory integrity protection to help prevent
|
||||||
@@ -10,12 +10,13 @@ malicious hypervisor-based attacks like data replay, memory-remapping and more
|
|||||||
in order to create an isolated execution environment. Here are some useful
|
in order to create an isolated execution environment. Here are some useful
|
||||||
links:
|
links:
|
||||||
|
|
||||||
- [SNP Homepage](https://docs.amd.com/v/u/en-US/amd-secure-encrypted-virtualization-solution-brief):
|
- [SNP Homepage](https://www.amd.com/content/dam/amd/en/documents/epyc-business-docs/solution-briefs/amd-secure-encrypted-virtualization-solution-brief.pdf):
|
||||||
more information about SEV-SNP technical aspects, design and specification.
|
more information about SEV-SNP technical aspects, design and specification.
|
||||||
|
|
||||||
## Cloud Hypervisor support
|
## Cloud Hypervisor support
|
||||||
|
|
||||||
A machine with AMD SEV-SNP support which is enabled in the BIOS is required.
|
It is required to use a machine which has enabled support for AMD SEV-SNP in
|
||||||
|
the BIOS.
|
||||||
|
|
||||||
On the Cloud Hypervisor side, all you need is to build the project with the
|
On the Cloud Hypervisor side, all you need is to build the project with the
|
||||||
`sev_snp` feature enabled:
|
`sev_snp` feature enabled:
|
||||||
@@ -25,7 +26,7 @@ cargo build --no-default-features --features "sev_snp"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Note**
|
**Note**
|
||||||
Please note that `sev_snp` cannot be enabled in conjunction with the `tdx` feature flag.
|
Please note that `sev_snp` cannot be enabled in conjunction with `tdx` feature flag.
|
||||||
|
|
||||||
You can run a SEV-SNP VM using the following command:
|
You can run a SEV-SNP VM using the following command:
|
||||||
|
|
||||||
@@ -37,4 +38,4 @@ You can run a SEV-SNP VM using the following command:
|
|||||||
--disk path=ubuntu.img
|
--disk path=ubuntu.img
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information related to Microsoft Hypervisor, please see [mshv.md](mshv.md)
|
For more information related to Microsoft Hypervisor please see [mshv.md](mshv.md)
|
||||||
|
|||||||
86
docs/api.md
86
docs/api.md
@@ -8,14 +8,14 @@
|
|||||||
- [REST API Examples](#rest-api-examples)
|
- [REST API Examples](#rest-api-examples)
|
||||||
- [Create a Virtual Machine](#create-a-virtual-machine)
|
- [Create a Virtual Machine](#create-a-virtual-machine)
|
||||||
- [Boot a Virtual Machine](#boot-a-virtual-machine)
|
- [Boot a Virtual Machine](#boot-a-virtual-machine)
|
||||||
- [Dump Virtual Machine Information](#dump-virtual-machine-information)
|
- [Dump a Virtual Machine Information](#dump-a-virtual-machine-information)
|
||||||
- [Reboot a Virtual Machine](#reboot-a-virtual-machine)
|
- [Reboot a Virtual Machine](#reboot-a-virtual-machine)
|
||||||
- [Shut a Virtual Machine Down](#shut-a-virtual-machine-down)
|
- [Shut a Virtual Machine Down](#shut-a-virtual-machine-down)
|
||||||
- [D-Bus API](#d-bus-api)
|
- [D-Bus API](#d-bus-api)
|
||||||
- [D-Bus API Location and availability](#d-bus-api-location-and-availability)
|
- [D-Bus API Location and availability](#d-bus-api-location-and-availability)
|
||||||
- [D-Bus API Interface](#d-bus-api-interface)
|
- [D-Bus API Interface](#d-bus-api-interface)
|
||||||
- [Command Line Interface](#command-line-interface)
|
- [Command Line Interface](#command-line-interface)
|
||||||
- [REST API, D-Bus API and CLI Architectural Relationship](#rest-api-d-bus-api-and-cli-architectural-relationship)
|
- [REST API, D-Bus API and CLI Architectural Relationship](#rest-api-and-cli-architectural-relationship)
|
||||||
- [Internal API](#internal-api)
|
- [Internal API](#internal-api)
|
||||||
- [Goals and Design](#goals-and-design)
|
- [Goals and Design](#goals-and-design)
|
||||||
- [End to End Example](#end-to-end-example)
|
- [End to End Example](#end-to-end-example)
|
||||||
@@ -31,7 +31,7 @@ The Cloud Hypervisor API is made of 2 distinct interfaces:
|
|||||||
|
|
||||||
1. **The internal API**, based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/)
|
1. **The internal API**, based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/)
|
||||||
module. This API is used internally by the Cloud Hypervisor threads to
|
module. This API is used internally by the Cloud Hypervisor threads to
|
||||||
communicate with each other.
|
communicate between each others.
|
||||||
|
|
||||||
The goal of this document is to describe the Cloud Hypervisor API as a whole,
|
The goal of this document is to describe the Cloud Hypervisor API as a whole,
|
||||||
and to outline how the internal and external APIs are architecturally related.
|
and to outline how the internal and external APIs are architecturally related.
|
||||||
@@ -72,37 +72,36 @@ The Cloud Hypervisor API exposes the following actions through its endpoints:
|
|||||||
##### Virtual Machine (VM) Actions
|
##### Virtual Machine (VM) Actions
|
||||||
|
|
||||||
| Action | Endpoint | Request Body | Response Body | Prerequisites |
|
| Action | Endpoint | Request Body | Response Body | Prerequisites |
|
||||||
| --------------------------------------- | ---------------------------- | --------------------------------- | ------------------------ | ------------------------------------------------------ |
|
| ---------------------------------- | ----------------------- | ------------------------------- | ------------------------ | ------------------------------------------------------ |
|
||||||
| Create the VM | `/vm.create` | `/schemas/VmConfig` | N/A | The VM is not created yet |
|
| Create the VM | `/vm.create` | `/schemas/VmConfig` | N/A | The VM is not created yet |
|
||||||
| Delete the VM | `/vm.delete` | N/A | N/A | N/A |
|
| Delete the VM | `/vm.delete` | N/A | N/A | N/A |
|
||||||
| Boot the VM | `/vm.boot` | N/A | N/A | The VM is created but not booted |
|
| Boot the VM | `/vm.boot` | N/A | N/A | The VM is created but not booted |
|
||||||
| Shut the VM down | `/vm.shutdown` | N/A | N/A | The VM is booted |
|
| Shut the VM down | `/vm.shutdown` | N/A | N/A | The VM is booted |
|
||||||
| Reboot the VM | `/vm.reboot` | N/A | N/A | The VM is booted |
|
| Reboot the VM | `/vm.reboot` | N/A | N/A | The VM is booted |
|
||||||
| Trigger power button of the VM | `/vm.power-button` | N/A | N/A | The VM is booted |
|
| Trigger power button of the VM | `/vm.power-button` | N/A | N/A | The VM is booted |
|
||||||
| Pause the VM | `/vm.pause` | N/A | N/A | The VM is booted |
|
| Pause the VM | `/vm.pause` | N/A | N/A | The VM is booted |
|
||||||
| Resume the VM | `/vm.resume` | N/A | N/A | The VM is paused |
|
| Resume the VM | `/vm.resume` | N/A | N/A | The VM is paused |
|
||||||
| Take a snapshot of the VM | `/vm.snapshot` | `/schemas/VmSnapshotConfig` | N/A | The VM is paused |
|
| Task a snapshot of the VM | `/vm.snapshot` | `/schemas/VmSnapshotConfig` | N/A | The VM is paused |
|
||||||
| Perform a coredump of the VM* | `/vm.coredump` | `/schemas/VmCoredumpData` | N/A | The VM is paused |
|
| Perform a coredump of the VM* | `/vm.coredump` | `/schemas/VmCoredumpData` | N/A | The VM is paused |
|
||||||
| Restore the VM from a snapshot | `/vm.restore` | `/schemas/RestoreConfig` | N/A | The VM is created but not booted |
|
| Restore the VM from a snapshot | `/vm.restore` | `/schemas/RestoreConfig` | N/A | The VM is created but not booted |
|
||||||
| Add/remove CPUs to/from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted |
|
| Add/remove CPUs to/from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted |
|
||||||
| Add/remove memory from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted |
|
| Add/remove memory from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted |
|
||||||
| Resize a disk attached to the VM | `/vm.resize-disk` | `/schemas/VmResizeDisk` | N/A | The VM is created |
|
| Resize a disk attached to the VM | `/vm.resize-disk` | `/schemas/VmResizeDisk` | N/A | The VM is created |
|
||||||
| Add/remove memory from a zone | `/vm.resize-zone` | `/schemas/VmResizeZone` | N/A | The VM is booted |
|
| Add/remove memory from a zone | `/vm.resize-zone` | `/schemas/VmResizeZone` | N/A | The VM is booted |
|
||||||
| Dump the VM information | `/vm.info` | N/A | `/schemas/VmInfo` | The VM is created |
|
| Dump the VM information | `/vm.info` | N/A | `/schemas/VmInfo` | The VM is created |
|
||||||
| Add VFIO PCI device to the VM | `/vm.add-device` | `/schemas/VmAddDevice` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add VFIO PCI device to the VM | `/vm.add-device` | `/schemas/VmAddDevice` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add disk device to the VM | `/vm.add-disk` | `/schemas/DiskConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add disk device to the VM | `/vm.add-disk` | `/schemas/DiskConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add fs device to the VM | `/vm.add-fs` | `/schemas/FsConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add fs device to the VM | `/vm.add-fs` | `/schemas/FsConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add generic vhost-user device to the VM | `/vm.add-generic-vhost-user` | `/schemas/GenericVhostUserConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add pmem device to the VM | `/vm.add-pmem` | `/schemas/PmemConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add pmem device to the VM | `/vm.add-pmem` | `/schemas/PmemConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add network device to the VM | `/vm.add-net` | `/schemas/NetConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add network device to the VM | `/vm.add-net` | `/schemas/NetConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add userspace PCI device to the VM | `/vm.add-user-device` | `/schemas/VmAddUserDevice` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add userspace PCI device to the VM | `/vm.add-user-device` | `/schemas/VmAddUserDevice` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add vdpa device to the VM | `/vm.add-vdpa` | `/schemas/VdpaConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add vdpa device to the VM | `/vm.add-vdpa` | `/schemas/VdpaConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Add vsock device to the VM | `/vm.add-vsock` | `/schemas/VsockConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
||||||
| Add vsock device to the VM | `/vm.add-vsock` | `/schemas/VsockConfig` | `/schemas/PciDeviceInfo` | The VM is booted |
|
| Remove device from the VM | `/vm.remove-device` | `/schemas/VmRemoveDevice` | N/A | The VM is booted |
|
||||||
| Remove device from the VM | `/vm.remove-device` | `/schemas/VmRemoveDevice` | N/A | The VM is booted |
|
| Dump the VM counters | `/vm.counters` | N/A | `/schemas/VmCounters` | The VM is booted |
|
||||||
| Dump the VM counters | `/vm.counters` | N/A | `/schemas/VmCounters` | The VM is booted |
|
| Inject an NMI | `/vm.nmi` | N/A | N/A | The VM is booted |
|
||||||
| Inject an NMI | `/vm.nmi` | N/A | N/A | The VM is booted |
|
| Prepare to receive a migration | `/vm.receive-migration` | `/schemas/ReceiveMigrationData` | N/A | N/A |
|
||||||
| Prepare to receive a migration | `/vm.receive-migration` | `/schemas/ReceiveMigrationData` | N/A | N/A |
|
| Start to send migration to target | `/vm.send-migration` | `/schemas/SendMigrationData` | N/A | The VM is booted and (shared mem or hugepages enabled) |
|
||||||
| Start to send migration to target | `/vm.send-migration` | `/schemas/SendMigrationData` | N/A | The VM is booted and (shared mem or hugepages enabled) |
|
|
||||||
|
|
||||||
* The `vmcoredump` action is available exclusively for the `x86_64`
|
* The `vmcoredump` action is available exclusively for the `x86_64`
|
||||||
architecture and can be executed only when the `guest_debug` feature is
|
architecture and can be executed only when the `guest_debug` feature is
|
||||||
@@ -156,9 +155,9 @@ Once the VM is created, we can boot it:
|
|||||||
curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.boot'
|
curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.boot'
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Dump Virtual Machine Information
|
##### Dump a Virtual Machine Information
|
||||||
|
|
||||||
We can fetch information about any VM as soon as it's created:
|
We can fetch information about any VM, as soon as it's created:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -202,7 +201,7 @@ see [D-Bus API Interface](#d-bus-api-interface).
|
|||||||
#### D-Bus API Location and availability
|
#### D-Bus API Location and availability
|
||||||
|
|
||||||
This feature is not compiled into Cloud Hypervisor by default. Users who
|
This feature is not compiled into Cloud Hypervisor by default. Users who
|
||||||
wish to use the D-Bus API must explicitly enable it with the `dbus_api`
|
wish to use the D-Bus API, must explicitly enable it with the `dbus_api`
|
||||||
feature flag when compiling Cloud Hypervisor.
|
feature flag when compiling Cloud Hypervisor.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -279,7 +278,7 @@ From the CLI, one can:
|
|||||||
The REST API, D-Bus API and the CLI all rely on a common, [internal API](#internal-api).
|
The REST API, D-Bus API and the CLI all rely on a common, [internal API](#internal-api).
|
||||||
|
|
||||||
The CLI options are parsed by the
|
The CLI options are parsed by the
|
||||||
[clap crate](https://docs.rs/clap/4.5.53/clap/) and then translated into
|
[clap crate](https://docs.rs/clap/4.3.11/clap/) and then translated into
|
||||||
[internal API](#internal-api) commands.
|
[internal API](#internal-api) commands.
|
||||||
|
|
||||||
The REST API is processed by an HTTP thread using the
|
The REST API is processed by an HTTP thread using the
|
||||||
@@ -289,7 +288,7 @@ crate. As with the CLI, the HTTP requests eventually get translated into
|
|||||||
|
|
||||||
The D-Bus API is implemented using the [zbus](https://github.com/dbus2/zbus)
|
The D-Bus API is implemented using the [zbus](https://github.com/dbus2/zbus)
|
||||||
crate and runs in its own thread. Whenever it needs to call the [internal API](#internal-api),
|
crate and runs in its own thread. Whenever it needs to call the [internal API](#internal-api),
|
||||||
the [blocking](https://github.com/smol-rs/blocking) crate is used to perform the call in zbus' async context.
|
the [blocking](https://github.com/smol-rs/blocking) crate is used perform the call in zbus' async context.
|
||||||
|
|
||||||
As a summary, the REST API, the D-Bus API and the CLI are essentially frontends for the
|
As a summary, the REST API, the D-Bus API and the CLI are essentially frontends for the
|
||||||
[internal API](#internal-api):
|
[internal API](#internal-api):
|
||||||
@@ -322,7 +321,7 @@ As a summary, the REST API, the D-Bus API and the CLI are essentially frontends
|
|||||||
|
|
||||||
The Cloud Hypervisor internal API, as its name suggests, is used internally
|
The Cloud Hypervisor internal API, as its name suggests, is used internally
|
||||||
by the different Cloud Hypervisor threads (VMM, HTTP, D-Bus, control loop,
|
by the different Cloud Hypervisor threads (VMM, HTTP, D-Bus, control loop,
|
||||||
etc) to send commands and responses to each other.
|
etc) to send commands and responses to each others.
|
||||||
|
|
||||||
It is based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/),
|
It is based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/),
|
||||||
and the single consumer (a.k.a. the API receiver) is the Cloud Hypervisor
|
and the single consumer (a.k.a. the API receiver) is the Cloud Hypervisor
|
||||||
@@ -366,8 +365,9 @@ APIs work together, let's look at a complete VM creation flow, from the
|
|||||||
[REST API](#rest-api) call, to the reply the external user will receive:
|
[REST API](#rest-api) call, to the reply the external user will receive:
|
||||||
|
|
||||||
1. A user or operator sends an HTTP request to the Cloud Hypervisor
|
1. A user or operator sends an HTTP request to the Cloud Hypervisor
|
||||||
[REST API](#rest-api) in order to create a virtual machine:
|
[REST API](#rest-api) in order to creates a virtual machine:
|
||||||
```shell
|
```
|
||||||
|
shell
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
curl --unix-socket /tmp/cloud-hypervisor.sock -i \
|
curl --unix-socket /tmp/cloud-hypervisor.sock -i \
|
||||||
@@ -414,7 +414,7 @@ APIs work together, let's look at a complete VM creation flow, from the
|
|||||||
the `VmCreate` payload, and extracts both the `VmConfig` structure and the
|
the `VmCreate` payload, and extracts both the `VmConfig` structure and the
|
||||||
[Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) from the
|
[Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) from the
|
||||||
command payload. It stores the `VmConfig` structure and replies back to the
|
command payload. It stores the `VmConfig` structure and replies back to the
|
||||||
sender (The HTTP thread):
|
sender ((The HTTP thread):
|
||||||
```Rust
|
```Rust
|
||||||
match api_request {
|
match api_request {
|
||||||
ApiRequest::VmCreate(config, sender) => {
|
ApiRequest::VmCreate(config, sender) => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user