mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Compare commits
11 Commits
v52.0
...
stable/v50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c17ccf49 | ||
|
|
cd4d6ded75 | ||
|
|
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
|
||||
allow:
|
||||
- dependency-type: all
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-minor-days: 7
|
||||
semver-patch-days: 3
|
||||
ignore:
|
||||
- dependency-name: "acpi_tables"
|
||||
- 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:
|
||||
paths: resources/Dockerfile
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./resources/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Build only
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./resources/Dockerfile
|
||||
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
|
||||
- name: Run metrics tests
|
||||
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
|
||||
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
|
||||
on:
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ARCH:
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
OS_DISK_SIZE:
|
||||
description: 'OS Disk Size in GB'
|
||||
required: true
|
||||
type: number
|
||||
type: string
|
||||
RG:
|
||||
description: 'Resource Group Name'
|
||||
required: true
|
||||
@@ -44,12 +44,13 @@ on:
|
||||
description: 'Private IP of the VM'
|
||||
value: ${{ jobs.infra-setup.outputs.PRIVATE_IP }}
|
||||
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
|
||||
jobs:
|
||||
infra-setup:
|
||||
name: ${{ inputs.ARCH }} VM Provision
|
||||
runs-on: mshv
|
||||
continue-on-error: true
|
||||
outputs:
|
||||
RG_NAME: ${{ steps.rg-setup.outputs.RG_NAME }}
|
||||
VM_NAME: ${{ steps.vm-setup.outputs.VM_NAME }}
|
||||
@@ -59,7 +60,7 @@ jobs:
|
||||
env:
|
||||
MI_CLIENT_ID: ${{ secrets.MI_CLIENT_ID }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Installing Azure CLI if not already installed"
|
||||
if ! command -v az &>/dev/null; then
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||
@@ -68,7 +69,7 @@ jobs:
|
||||
fi
|
||||
az --version
|
||||
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
|
||||
id: get-location
|
||||
@@ -76,13 +77,13 @@ jobs:
|
||||
SKU: ${{ inputs.VM_SKU }}
|
||||
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
# Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2)
|
||||
if ! [[ "$SKU" =~ ^Standard_[A-Za-z]+([1-9][0-9]*) ]]; then
|
||||
printf 'Cannot extract vCPU count from SKU: %q\n' "$SKU"
|
||||
vcpu=$(echo "$SKU" | sed -n 's/^Standard_[A-Za-z]\+\([0-9]\+\).*/\1/p')
|
||||
if [[ -z "$vcpu" ]]; then
|
||||
echo "Cannot extract vCPU count from SKU: $SKU"
|
||||
exit 1
|
||||
fi
|
||||
vcpu=${BASH_REMATCH[1]}
|
||||
|
||||
SUPPORTED_LOCATIONS=$(echo "$STORAGE_ACCOUNT_PATHS" | jq -r 'to_entries[] | .key')
|
||||
|
||||
@@ -93,9 +94,11 @@ jobs:
|
||||
continue
|
||||
fi
|
||||
|
||||
remaining=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json |
|
||||
jq '(.limit | tonumber) - (.currentValue | tonumber) >= ($ARGS.positional[0] | tonumber)' --jsonargs "$vcpu")
|
||||
if [[ "$remaining" = true ]]; then
|
||||
usage=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json)
|
||||
current=$(echo "$usage" | jq -r '.currentValue')
|
||||
limit=$(echo "$usage" | jq -r '.limit')
|
||||
|
||||
if [[ $((limit - current)) -ge $vcpu ]]; then
|
||||
echo "Sufficient quota found in $location"
|
||||
echo "location=$location" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
@@ -112,11 +115,11 @@ jobs:
|
||||
RG: ${{ inputs.RG }}
|
||||
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Creating Resource Group: $RG"
|
||||
# Create the resource group
|
||||
echo "Creating resource group in location: ${LOCATION}"
|
||||
az group create --name "${RG}" --location "${LOCATION}"
|
||||
az group create --name ${RG} --location ${LOCATION}
|
||||
echo "RG_NAME=${RG}" >> $GITHUB_OUTPUT
|
||||
echo "Resource group created successfully."
|
||||
|
||||
@@ -125,10 +128,10 @@ jobs:
|
||||
env:
|
||||
KEY: ${{ inputs.KEY }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Generating SSH key: $KEY"
|
||||
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
|
||||
id: vm-setup
|
||||
@@ -143,12 +146,12 @@ jobs:
|
||||
VM_IMAGE_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_image
|
||||
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Creating $VM_SKU VM: $VM_NAME"
|
||||
|
||||
# Extract subnet ID from the runner VM
|
||||
echo "Retrieving subnet ID..."
|
||||
SUBNET_ID=$(az network vnet list --resource-group "$RUNNER_RG" --query "[?contains(location, '${LOCATION}')].{SUBNETS:subnets}" | jq -r ".[0].SUBNETS[0].id")
|
||||
SUBNET_ID=$(az network vnet list --resource-group ${RUNNER_RG} --query "[?contains(location, '${LOCATION}')].{SUBNETS:subnets}" | jq -r ".[0].SUBNETS[0].id")
|
||||
if [[ -z "${SUBNET_ID}" ]]; then
|
||||
echo "ERROR: Failed to retrieve Subnet ID."
|
||||
exit 1
|
||||
@@ -156,7 +159,7 @@ jobs:
|
||||
|
||||
# Extract image ID from the runner VM
|
||||
echo "Retrieving image ID..."
|
||||
IMAGE_ID=$(az image show --resource-group "$RUNNER_RG" --name "$VM_IMAGE_NAME" --query "id" -o tsv)
|
||||
IMAGE_ID=$(az image show --resource-group ${RUNNER_RG} --name ${VM_IMAGE_NAME} --query "id" -o tsv)
|
||||
if [[ -z "${IMAGE_ID}" ]]; then
|
||||
echo "ERROR: Failed to retrieve Image ID."
|
||||
exit 1
|
||||
@@ -164,24 +167,24 @@ jobs:
|
||||
|
||||
# Create VM
|
||||
az vm create \
|
||||
--resource-group "${RG}" \
|
||||
--name "${VM_NAME}" \
|
||||
--subnet "${SUBNET_ID}" \
|
||||
--size "${VM_SKU}" \
|
||||
--location "${LOCATION}" \
|
||||
--image "${IMAGE_ID}" \
|
||||
--os-disk-size-gb "${OS_DISK_SIZE}" \
|
||||
--resource-group ${RG} \
|
||||
--name ${VM_NAME} \
|
||||
--subnet ${SUBNET_ID} \
|
||||
--size ${VM_SKU} \
|
||||
--location ${LOCATION} \
|
||||
--image ${IMAGE_ID} \
|
||||
--os-disk-size-gb ${OS_DISK_SIZE} \
|
||||
--public-ip-sku Standard \
|
||||
--storage-sku Premium_LRS \
|
||||
--public-ip-address "" \
|
||||
--admin-username "${USERNAME}" \
|
||||
--ssh-key-value ~/.ssh/"${KEY}".pub \
|
||||
--admin-username ${USERNAME} \
|
||||
--ssh-key-value ~/.ssh/${KEY}.pub \
|
||||
--security-type Standard \
|
||||
--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."
|
||||
|
||||
- name: Get VM Private IP
|
||||
@@ -190,15 +193,15 @@ jobs:
|
||||
RG: ${{ inputs.RG }}
|
||||
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Retrieving VM Private IP address..."
|
||||
# Retrieve VM Private IP address
|
||||
PRIVATE_IP=$(az vm show -g "${RG}" -n "${VM_NAME}" -d --query privateIps -o tsv)
|
||||
PRIVATE_IP=$(az vm show -g ${RG} -n ${VM_NAME} -d --query privateIps -o tsv)
|
||||
if [[ -z "$PRIVATE_IP" ]]; then
|
||||
echo "ERROR: Failed to retrieve private IP address."
|
||||
exit 1
|
||||
fi
|
||||
echo "PRIVATE_IP=$PRIVATE_IP" >> "$GITHUB_OUTPUT"
|
||||
echo "PRIVATE_IP=$PRIVATE_IP" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Wait for SSH availability
|
||||
env:
|
||||
@@ -207,16 +210,16 @@ jobs:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
run: |
|
||||
echo "Waiting for SSH to be accessible..."
|
||||
timeout 120 bash -c 'until ssh -o StrictHostKeyChecking=no -i ~/.ssh/"${KEY}" -- "${USERNAME}@${PRIVATE_IP}" "exit" 2>/dev/null; do sleep 5; done'
|
||||
timeout 120 bash -c 'until ssh -o StrictHostKeyChecking=no -i ~/.ssh/${KEY} ${USERNAME}@${PRIVATE_IP} "exit" 2>/dev/null; do sleep 5; done'
|
||||
echo "VM is accessible!"
|
||||
|
||||
- name: Remove Old Host Key
|
||||
env:
|
||||
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
echo "Removing the old host key"
|
||||
ssh-keygen -R "$PRIVATE_IP"
|
||||
ssh-keygen -R $PRIVATE_IP
|
||||
|
||||
- name: SSH into VM and Install Dependencies
|
||||
env:
|
||||
@@ -224,9 +227,9 @@ jobs:
|
||||
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" << EOF
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF
|
||||
set -e
|
||||
echo "Logged in successfully."
|
||||
echo "Installing dependencies..."
|
||||
sudo tdnf install -y git moby-engine moby-cli clang llvm pkg-config make gcc glibc-devel
|
||||
@@ -241,6 +244,6 @@ jobs:
|
||||
sudo systemctl enable containerd.service
|
||||
sudo systemctl start docker
|
||||
sudo groupadd -f docker
|
||||
sudo usermod -a -G docker "${USERNAME}"
|
||||
sudo usermod -a -G docker ${USERNAME}
|
||||
sudo systemctl restart docker
|
||||
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)
|
||||
on: [pull_request_target, merge_group]
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
infra-setup:
|
||||
@@ -24,6 +23,7 @@ jobs:
|
||||
needs: infra-setup
|
||||
if: ${{ always() && needs.infra-setup.result == 'success' }}
|
||||
runs-on: mshv
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Run integration tests
|
||||
timeout-minutes: 60
|
||||
@@ -36,9 +36,9 @@ jobs:
|
||||
RG: MSHV-${{ github.run_id }}
|
||||
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
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
|
||||
echo "Logged in successfully."
|
||||
export PATH="\$HOME/.cargo/bin:\$PATH"
|
||||
@@ -67,12 +67,12 @@ jobs:
|
||||
|
||||
echo "Setting permissions..."
|
||||
for i in 0 1 2; do
|
||||
dev="/dev/vhost-vdpa-\$i"
|
||||
if [ -e "\$dev" ]; then
|
||||
sudo chown \$USER:\$USER "\$dev"
|
||||
sudo chmod 660 "\$dev"
|
||||
dev="/dev/vhost-vdpa-$i"
|
||||
if [ -e "$dev" ]; then
|
||||
sudo chown $USER:$USER "$dev"
|
||||
sudo chmod 660 "$dev"
|
||||
else
|
||||
echo "Warning: Device \$dev not found"
|
||||
echo "Warning: Device $dev not found"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -87,7 +87,9 @@ jobs:
|
||||
PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }}
|
||||
USERNAME: ${{ secrets.MSHV_USERNAME }}
|
||||
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
|
||||
if: always()
|
||||
@@ -96,7 +98,7 @@ jobs:
|
||||
RG_NAME: ${{ needs.infra-setup.outputs.RG_NAME }}
|
||||
VM_NAME: ${{ needs.infra-setup.outputs.VM_NAME }}
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
set -e
|
||||
az vm boot-diagnostics get-boot-log --name "${VM_NAME}" --resource-group "${RG_NAME}" | jq -r
|
||||
|
||||
cleanup:
|
||||
@@ -109,8 +111,8 @@ jobs:
|
||||
env:
|
||||
RG: MSHV-INTEGRATION-${{ github.run_id }}
|
||||
run: |
|
||||
if az group exists --name "${RG}"; then
|
||||
az group delete --name "${RG}" --yes --no-wait
|
||||
if az group exists --name ${RG}; then
|
||||
az group delete --name ${RG} --yes --no-wait
|
||||
else
|
||||
echo "Resource Group ${RG} does not exist. Skipping deletion."
|
||||
fi
|
||||
@@ -120,8 +122,8 @@ jobs:
|
||||
env:
|
||||
KEY: azure_key_${{ github.run_id }}
|
||||
run: |
|
||||
if [ -f ~/.ssh/"${KEY}" ]; then
|
||||
rm -f ~/.ssh/"${KEY}" ~/.ssh/"${KEY}.pub"
|
||||
if [ -f ~/.ssh/${KEY} ]; then
|
||||
rm -f ~/.ssh/${KEY} ~/.ssh/${KEY}.pub
|
||||
echo "SSH key deleted successfully."
|
||||
else
|
||||
echo "SSH key does not exist. Skipping deletion."
|
||||
|
||||
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
|
||||
on: [create, merge_group]
|
||||
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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }}
|
||||
- name: Upload Release Artifacts
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Artifacts for ${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -80,13 +80,13 @@ jobs:
|
||||
github.event_name == 'create' && github.event.ref_type == 'tag' &&
|
||||
matrix.platform.target == 'x86_64-unknown-linux-gnu'
|
||||
id: upload-release-cloud-hypervisor-vendored-sources
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
||||
name: cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
||||
- name: Create GitHub Release
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
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
|
||||
**/Cargo.lock
|
||||
**/rusty-tags.vi
|
||||
/.agents
|
||||
/.cargo
|
||||
/.claude
|
||||
/.codex
|
||||
/.vscode
|
||||
/build
|
||||
/rpm/SOURCES
|
||||
/target
|
||||
/.vscode
|
||||
/vendor
|
||||
__pycache__
|
||||
|
||||
38
.lychee.toml
38
.lychee.toml
@@ -1,33 +1,27 @@
|
||||
verbose = "info"
|
||||
|
||||
exclude_path = [".lychee.toml"]
|
||||
|
||||
exclude = [
|
||||
# Availability of links below should be manually verified.
|
||||
# Page for intel TDX support, returns 403 while querying.
|
||||
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
|
||||
# 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',
|
||||
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
|
||||
'^https://github.com/smibarber',
|
||||
# OSDev has added bot protection and accesses my result in 403 Forbidden.
|
||||
'^https://wiki.osdev.org',
|
||||
# Exclude all pages with $ in the URL since $XXX is a variable
|
||||
"\\$.*",
|
||||
# Exclude local files
|
||||
"file://.*",
|
||||
# ARM documentation returns 403 Forbidden for automated CI checks.
|
||||
'^http://infocenter\.arm\.com',
|
||||
'^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/',
|
||||
# Availability of links below should be manually verified.
|
||||
# Page for intel TDX support, returns 403 while querying.
|
||||
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
|
||||
# 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',
|
||||
|
||||
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
|
||||
'^https://github.com/smibarber',
|
||||
|
||||
# OSDev has added bot protection and accesses my result in 403 Forbidden.
|
||||
'^https://wiki.osdev.org',
|
||||
# Exclude all pages with $ in the URL since $XXX is a variable
|
||||
"\\$.*",
|
||||
# Exclude local files
|
||||
"file://.*",
|
||||
]
|
||||
|
||||
# Exclude loopback addresses
|
||||
exclude_loopback = true
|
||||
|
||||
|
||||
max_retries = 3
|
||||
|
||||
retry_wait_time = 5
|
||||
|
||||
@@ -7,6 +7,6 @@ Files: docs/*.md *.md
|
||||
Copyright: 2024
|
||||
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
|
||||
License: Apache-2.0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include = ["**/Cargo.toml"]
|
||||
|
||||
[formatting]
|
||||
indent_string = " " # 2 spaces: keep in sync with .editorconfig
|
||||
reorder_arrays = true
|
||||
reorder_keys = true
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"hypervisor/src/kvm/x86_64/mod.rs",
|
||||
"resources/linux-config-*",
|
||||
"hypervisor/src/kvm/x86_64/mod.rs",
|
||||
"resources/linux-config-*",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
@@ -21,8 +21,5 @@ liness = "liness"
|
||||
outout = "outout"
|
||||
|
||||
[default.extend-identifiers]
|
||||
consts = "consts"
|
||||
fo = "fo"
|
||||
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
|
||||
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,
|
||||
including `rustfmt`, `clippy`, and other common Rust quality checks, for every
|
||||
pull request. We adapt to best practices, new lints and new tooling as the
|
||||
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
|
||||
We follow the [Rust Style](https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src)
|
||||
convention and enforce it through the Continuous Integration (CI) process calling into `rustfmt`,
|
||||
`clippy`, and other well-known code quality tool of the ecosystem for each submitted Pull Request (PR).
|
||||
|
||||
## Basic Checks
|
||||
|
||||
```sh
|
||||
# We currently rely on nightly-only formatting features
|
||||
cargo +nightly fmt --all
|
||||
cargo +nightly fmt --all
|
||||
cargo check --all-targets --tests
|
||||
cargo clippy --all-targets --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._
|
||||
|
||||
```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
|
||||
@@ -77,65 +58,42 @@ commit you make.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
- Follow the pattern: \
|
||||
```
|
||||
<component>: Change summary
|
||||
More detailed explanation of your changes: Why and how.
|
||||
Wrap it to 72 characters.
|
||||
See http://chris.beams.io/posts/git-commit/
|
||||
for some more good pieces of advice.
|
||||
|
||||
More detailed explanation of your changes: Why and how.
|
||||
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>
|
||||
```
|
||||
|
||||
Signed-off-by: <contributor@foo.com>
|
||||
```
|
||||
|
||||
|
||||
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:
|
||||
For example:
|
||||
|
||||
```
|
||||
vm-virtio: Reset underlying device on driver request
|
||||
|
||||
|
||||
If the driver triggers a reset by writing zero into the status register
|
||||
then reset the underlying device if supported. A device reset also
|
||||
requires resetting various aspects of the queue.
|
||||
|
||||
|
||||
In order to be able to do a subsequent reactivate it is required to
|
||||
reclaim certain resources (interrupt and queue EventFDs.) If a device
|
||||
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
|
||||
configure it anyway.
|
||||
|
||||
|
||||
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
|
||||
|
||||
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. [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.
|
||||
1. Each commit must comply with the Commit Hygiene guidelines above.
|
||||
1. A pull request should address a single component or concern to keep review
|
||||
focused and approvals straightforward.
|
||||
1. To update your pull request amend existing commits whenever applicable and
|
||||
then push the new changes to your pull request branch.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
VMM responsibility to set the terminal back into canonical mode if we
|
||||
don't want to get any weird behavior from the shell.
|
||||
|
||||
|
||||
Fixes #88
|
||||
|
||||
|
||||
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
|
||||
[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
|
||||
sound engineering judgment. Please use AI/LLM-assisted tooling thoughtfully and
|
||||
responsibly to ensure efficient use of limited project resources, particularly
|
||||
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.
|
||||
Our policy is to decline any contributions known to contain contents
|
||||
generated or derived from using Large Language Models (LLMs). This
|
||||
includes ChatGPT, Gemini, Claude, Copilot and similar tools.
|
||||
|
||||
Or in other words: please apply common sense and don't blindly accept LLM
|
||||
suggestions.
|
||||
|
||||
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.
|
||||
The goal is to avoid ambiguity in license compliance and optimize the
|
||||
use of limited project resources, especially for code review and
|
||||
maintenance. This policy can be revisited as LLMs evolve and mature.
|
||||
|
||||
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",
|
||||
"event_monitor",
|
||||
"hypervisor",
|
||||
"net_gen",
|
||||
"net_util",
|
||||
"option_parser",
|
||||
"pci",
|
||||
@@ -40,67 +41,57 @@ members = [
|
||||
"vmm",
|
||||
]
|
||||
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"
|
||||
|
||||
[workspace.dependencies]
|
||||
# rust-vmm crates
|
||||
acpi_tables = "0.2.0"
|
||||
iommufd-ioctls = "0.1.0"
|
||||
kvm-bindings = "0.14.0"
|
||||
kvm-ioctls = "0.24.0"
|
||||
linux-loader = "0.13.2"
|
||||
mshv-bindings = "0.6.9"
|
||||
mshv-ioctls = "0.6.9"
|
||||
acpi_tables = { git = "https://github.com/rust-vmm/acpi_tables", branch = "main" }
|
||||
kvm-bindings = "0.12.1"
|
||||
kvm-ioctls = "0.22.1"
|
||||
linux-loader = "0.13.1"
|
||||
mshv-bindings = "0.6.5"
|
||||
mshv-ioctls = "0.6.5"
|
||||
seccompiler = "0.5.0"
|
||||
vfio-bindings = { version = "0.6.2", default-features = false }
|
||||
vfio-ioctls = { version = "0.6.0", default-features = false }
|
||||
vfio_user = { version = "0.1.3", default-features = false }
|
||||
vhost = { version = "0.16.0", default-features = false }
|
||||
vhost-user-backend = { version = "0.22.0", default-features = false }
|
||||
vfio-bindings = { version = "0.6.0", default-features = false }
|
||||
vfio-ioctls = { version = "0.5.1", default-features = false }
|
||||
vfio_user = { version = "0.1.1", default-features = false }
|
||||
vhost = { version = "0.14.0", default-features = false }
|
||||
vhost-user-backend = { version = "0.20.0", default-features = false }
|
||||
virtio-bindings = "0.2.6"
|
||||
virtio-queue = "0.17.0"
|
||||
virtio-queue = "0.16.0"
|
||||
vm-fdt = "0.3.0"
|
||||
vm-memory = "0.17.1"
|
||||
vmm-sys-util = "0.15.0"
|
||||
vm-memory = "0.16.1"
|
||||
vmm-sys-util = "0.14.0"
|
||||
|
||||
# igvm crates
|
||||
igvm = "0.4.0"
|
||||
igvm_defs = "0.4.0"
|
||||
# TODO: bump to 0.3.5 release
|
||||
igvm = { git = "https://github.com/microsoft/igvm", branch = "main" }
|
||||
igvm_defs = { git = "https://github.com/microsoft/igvm", branch = "main" }
|
||||
|
||||
# serde crates
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
serde_with = { version = "3.18.0", default-features = false }
|
||||
serde_json = "1.0.145"
|
||||
serde_with = { version = "3.16.1", default-features = false }
|
||||
|
||||
# other crates
|
||||
anyhow = "1.0.102"
|
||||
bitflags = "2.11.1"
|
||||
anyhow = "1.0.99"
|
||||
bitflags = "2.10.0"
|
||||
byteorder = "1.5.0"
|
||||
cfg-if = "1.0.4"
|
||||
clap = "4.6.1"
|
||||
clap = "4.5.53"
|
||||
dhat = "0.3.3"
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11.10"
|
||||
env_logger = "0.11.8"
|
||||
epoll = "4.4.0"
|
||||
flume = "0.12.0"
|
||||
itertools = "0.14.0"
|
||||
jiff = { version = "0.2", default-features = false, features = ["std"] }
|
||||
libc = "0.2.186"
|
||||
libc = "0.2.178"
|
||||
log = "0.4.29"
|
||||
sha2 = "0.11.0"
|
||||
signal-hook = "0.4.4"
|
||||
thiserror = "2.0.18"
|
||||
uuid = { version = "1.23.1" }
|
||||
signal-hook = "0.3.18"
|
||||
thiserror = "2.0.17"
|
||||
uuid = { version = "1.19.0" }
|
||||
wait-timeout = "0.2.1"
|
||||
zerocopy = { version = "0.8.48", default-features = false }
|
||||
zerocopy = { version = "0.8.31", default-features = false }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# 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
|
||||
|
||||
Cloud Hypervisor boots guests in one of two ways. The first is direct
|
||||
kernel boot, where a kernel image is passed to `--kernel`. The x86-64
|
||||
kernel must be built with PVH support or be a bzImage. The second is
|
||||
firmware boot, where a firmware image is passed to `--firmware` and
|
||||
brings up the guest's normal boot loader.
|
||||
Cloud Hypervisor supports direct kernel boot (the x86-64 kernel requires the kernel
|
||||
built with PVH support or a bzImage) or booting via a firmware (either [Rust Hypervisor
|
||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) or an
|
||||
edk2 UEFI firmware called `CLOUDHV` / `CLOUDHV_EFI`.)
|
||||
|
||||
Two firmware options are supported, and which one works best depends
|
||||
on the guest OS. [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
|
||||
Binary builds of the firmware files are available for the latest release of
|
||||
[Rust Hypervisor
|
||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest)
|
||||
and [our edk2
|
||||
fork](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
|
||||
with upstream tianocore/edk2.
|
||||
repository](https://github.com/cloud-hypervisor/edk2/releases/latest)
|
||||
|
||||
The choice of firmware depends on your guest OS choice; some experimentation
|
||||
may be required.
|
||||
|
||||
### Firmware Booting
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
authors = ["The Cloud Hypervisor Authors"]
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
name = "api_client"
|
||||
rust-version.workspace = true
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
authors = ["The Chromium OS Authors"]
|
||||
edition.workspace = true
|
||||
name = "arch"
|
||||
rust-version.workspace = true
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -19,7 +19,7 @@ use hypervisor::arch::aarch64::regs::{
|
||||
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,
|
||||
};
|
||||
use log::{debug, info, warn};
|
||||
use log::{debug, warn};
|
||||
use thiserror::Error;
|
||||
use vm_fdt::{FdtWriter, FdtWriterResult};
|
||||
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
|
||||
@@ -345,17 +345,6 @@ fn create_cpu_nodes(
|
||||
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) {
|
||||
let cpu_name = format!("cpu@{cpu_id:x}");
|
||||
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("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?;
|
||||
|
||||
// Skipping NUMA encoding in FDT when Generic Initiator devices
|
||||
// are present allowed such guest kernels to boot properly and
|
||||
// rely solely on ACPI tables to setup NUMA
|
||||
if numa_nodes.len() > 1 && !has_generic_initiator {
|
||||
// Add `numa-node-id` property if there is any numa config.
|
||||
if numa_nodes.len() > 1 {
|
||||
for numa_node_idx in 0..numa_nodes.len() {
|
||||
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||
if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) {
|
||||
@@ -514,14 +501,7 @@ fn create_memory_node(
|
||||
) -> FdtWriterResult<()> {
|
||||
// See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt
|
||||
// for NUMA setting in memory node.
|
||||
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
|
||||
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 {
|
||||
if numa_nodes.len() > 1 {
|
||||
for numa_node_idx in 0..numa_nodes.len() {
|
||||
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||
let mut mem_reg_prop: Vec<u64> = Vec::new();
|
||||
@@ -538,15 +518,12 @@ fn create_memory_node(
|
||||
node_memory_addr = memory_region_start_addr;
|
||||
}
|
||||
}
|
||||
// Only create a memory node if this NUMA node has memory regions
|
||||
if !mem_reg_prop.is_empty() {
|
||||
let memory_node_name = format!("memory@{node_memory_addr:x}");
|
||||
let memory_node = fdt.begin_node(&memory_node_name)?;
|
||||
fdt.property_string("device_type", "memory")?;
|
||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
|
||||
fdt.end_node(memory_node)?;
|
||||
}
|
||||
let memory_node_name = format!("memory@{node_memory_addr:x}");
|
||||
let memory_node = fdt.begin_node(&memory_node_name)?;
|
||||
fdt.property_string("device_type", "memory")?;
|
||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
|
||||
fdt.end_node(memory_node)?;
|
||||
}
|
||||
} else {
|
||||
// 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<()> {
|
||||
// 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")?;
|
||||
fdt.property_string("compatible", "numa-distance-map-v1")?;
|
||||
// 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.
|
||||
// 4. distance-matrix should have entries in lexicographical ascending
|
||||
// order of nodes.
|
||||
numa_ids.sort_unstable(); // lexicographical order
|
||||
let mut distance_matrix = Vec::new();
|
||||
// Iterate over actual numa IDs instead of 0..len()
|
||||
for numa_id in numa_ids.iter() {
|
||||
let numa_node = &numa_nodes[numa_id];
|
||||
for dest_numa_id in numa_ids.iter() {
|
||||
if *numa_id == *dest_numa_id {
|
||||
distance_matrix.push(*numa_id);
|
||||
distance_matrix.push(*dest_numa_id);
|
||||
for numa_node_idx in 0..numa_nodes.len() {
|
||||
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
|
||||
for dest_numa_node in 0..numa_node.unwrap().distances.len() + 1 {
|
||||
if numa_node_idx == dest_numa_node {
|
||||
distance_matrix.push(numa_node_idx as u32);
|
||||
distance_matrix.push(dest_numa_node as u32);
|
||||
distance_matrix.push(10_u32);
|
||||
continue;
|
||||
}
|
||||
|
||||
distance_matrix.push(*numa_id);
|
||||
distance_matrix.push(*dest_numa_id);
|
||||
// Use user-specified distance, checking both directions for symmetry
|
||||
let distance = if let Some(&dist) = numa_node.distances.get(dest_numa_id) {
|
||||
// Forward direction: current node -> dest node
|
||||
dist
|
||||
} else if let Some(dest_node) = numa_nodes.get(dest_numa_id) {
|
||||
// Reverse direction for symmetry: dest node -> current node
|
||||
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);
|
||||
distance_matrix.push(numa_node_idx as u32);
|
||||
distance_matrix.push(dest_numa_node as u32);
|
||||
distance_matrix.push(
|
||||
*numa_node
|
||||
.unwrap()
|
||||
.distances
|
||||
.get(&(dest_numa_node as u32))
|
||||
.unwrap() as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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 thiserror::Error;
|
||||
use vm_memory::{Bytes, GuestAddress, GuestMemory};
|
||||
use vm_memory::{GuestAddress, GuestMemory};
|
||||
|
||||
/// Errors thrown while loading UEFI binary
|
||||
#[derive(Debug, Error)]
|
||||
|
||||
@@ -120,7 +120,6 @@ pub struct NumaNode {
|
||||
pub pci_segments: Vec<u16>,
|
||||
pub distances: BTreeMap<u32, u8>,
|
||||
pub memory_zones: Vec<String>,
|
||||
pub device_id: Option<String>,
|
||||
}
|
||||
|
||||
pub type NumaNodes = BTreeMap<u32, NumaNode>;
|
||||
|
||||
@@ -641,16 +641,17 @@ pub fn generate_common_cpuid(
|
||||
|
||||
// Update some existing CPUID
|
||||
for entry in cpuid.as_mut_slice().iter_mut() {
|
||||
#[allow(unused_unsafe)]
|
||||
match entry.function {
|
||||
// Clear AMX related bits if the AMX feature is not enabled
|
||||
0x7 if !config.amx => {
|
||||
if entry.index == 0 {
|
||||
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
|
||||
}
|
||||
if entry.index == 1 {
|
||||
entry.eax &= !(1 << AMX_FP16);
|
||||
entry.edx &= !(1 << AMX_COMPLEX);
|
||||
0x7 => {
|
||||
if !config.amx {
|
||||
if entry.index == 0 {
|
||||
entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8));
|
||||
}
|
||||
if entry.index == 1 {
|
||||
entry.eax &= !(1 << AMX_FP16);
|
||||
entry.edx &= !(1 << AMX_COMPLEX);
|
||||
}
|
||||
}
|
||||
}
|
||||
0xd =>
|
||||
@@ -672,52 +673,55 @@ pub fn generate_common_cpuid(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tile Information (purely AMX related).
|
||||
0x1d if !config.amx => {
|
||||
entry.eax = 0;
|
||||
entry.ebx = 0;
|
||||
entry.ecx = 0;
|
||||
entry.edx = 0;
|
||||
0x1d => {
|
||||
// Tile Information (purely AMX related).
|
||||
if !config.amx {
|
||||
entry.eax = 0;
|
||||
entry.ebx = 0;
|
||||
entry.ecx = 0;
|
||||
entry.edx = 0;
|
||||
}
|
||||
}
|
||||
// TMUL information (purely AMX related)
|
||||
0x1e if !config.amx => {
|
||||
entry.eax = 0;
|
||||
entry.ebx = 0;
|
||||
entry.ecx = 0;
|
||||
entry.edx = 0;
|
||||
0x1e => {
|
||||
// TMUL information (purely AMX related)
|
||||
if !config.amx {
|
||||
entry.eax = 0;
|
||||
entry.ebx = 0;
|
||||
entry.ecx = 0;
|
||||
entry.edx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy host L1 cache details if not populated by KVM
|
||||
0x8000_0005
|
||||
if entry.eax == 0
|
||||
&& entry.ebx == 0
|
||||
&& entry.ecx == 0
|
||||
&& entry.edx == 0
|
||||
0x8000_0005 => {
|
||||
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
|
||||
#[allow(unused_unsafe)]
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
|
||||
{
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
|
||||
entry.eax = leaf.eax;
|
||||
entry.ebx = leaf.ebx;
|
||||
entry.ecx = leaf.ecx;
|
||||
entry.edx = leaf.edx;
|
||||
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 {
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
|
||||
entry.eax = leaf.eax;
|
||||
entry.ebx = leaf.ebx;
|
||||
entry.ecx = leaf.ecx;
|
||||
entry.edx = leaf.edx;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy host L2 cache details if not populated by KVM
|
||||
0x8000_0006
|
||||
if entry.eax == 0
|
||||
&& entry.ebx == 0
|
||||
&& entry.ecx == 0
|
||||
&& entry.edx == 0
|
||||
0x8000_0006 => {
|
||||
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 {
|
||||
#[allow(unused_unsafe)]
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
|
||||
{
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
|
||||
entry.eax = leaf.eax;
|
||||
entry.ebx = leaf.ebx;
|
||||
entry.ecx = leaf.ecx;
|
||||
entry.edx = leaf.edx;
|
||||
if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 {
|
||||
#[allow(unused_unsafe)]
|
||||
// SAFETY: cpuid called with valid leaves
|
||||
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
|
||||
entry.eax = leaf.eax;
|
||||
entry.ebx = leaf.ebx;
|
||||
entry.ecx = leaf.ecx;
|
||||
entry.edx = leaf.edx;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set CPU physical bits
|
||||
0x8000_0008 => {
|
||||
@@ -819,7 +823,6 @@ pub fn configure_vcpu(
|
||||
cpu_vendor: CpuVendor,
|
||||
topology: (u16, u16, u16, u16),
|
||||
nested: bool,
|
||||
setup_registers: bool,
|
||||
) -> super::Result<()> {
|
||||
let x2apic_id = get_x2apic_id(id, Some(topology));
|
||||
|
||||
@@ -838,13 +841,11 @@ pub fn configure_vcpu(
|
||||
entry.ebx &= 0xffffff;
|
||||
entry.ebx |= x2apic_id << 24;
|
||||
apic_id_patched = true;
|
||||
if matches!(cpu_vendor, CpuVendor::Intel) {
|
||||
if !nested {
|
||||
// Disable nested virtualization for Intel
|
||||
entry.ecx &= !(1 << VMX_ECX_BIT);
|
||||
}
|
||||
break;
|
||||
if !nested {
|
||||
// Disable nested virtualization for Intel
|
||||
entry.ecx &= !(1 << VMX_ECX_BIT);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if entry.function == 0x8000_0001 {
|
||||
if !nested {
|
||||
@@ -893,19 +894,17 @@ pub fn configure_vcpu(
|
||||
|
||||
regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?;
|
||||
if let Some((kernel_entry_point, guest_memory)) = boot_setup {
|
||||
if setup_registers {
|
||||
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_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?;
|
||||
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()))?;
|
||||
Ok(())
|
||||
|
||||
@@ -101,9 +101,8 @@ const CPU_FEATURE_APIC: u32 = 0x200;
|
||||
const CPU_FEATURE_FPU: u32 = 0x001;
|
||||
|
||||
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`.
|
||||
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;
|
||||
for i in v_slice.iter() {
|
||||
checksum = checksum.wrapping_add(*i);
|
||||
|
||||
@@ -33,8 +33,8 @@ pub enum Error {
|
||||
#[error("Failure to write additional data to memory")]
|
||||
WriteData,
|
||||
/// Failure to parse uuid, uuid format may be error
|
||||
#[error("Failure to parse uuid: {1}")]
|
||||
ParseUuid(#[source] uuid::Error, String),
|
||||
#[error("Failure to parse uuid")]
|
||||
ParseUuid(#[source] uuid::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;
|
||||
|
||||
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`.
|
||||
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;
|
||||
for i in v_slice.iter() {
|
||||
checksum = checksum.wrapping_add(*i);
|
||||
@@ -199,7 +198,7 @@ pub fn setup_smbios(
|
||||
let uuid_number = uuid
|
||||
.map(Uuid::parse_str)
|
||||
.transpose()
|
||||
.map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))?
|
||||
.map_err(Error::ParseUuid)?
|
||||
.unwrap_or(Uuid::nil());
|
||||
let smbios_sysinfo = SmbiosSysInfo {
|
||||
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
|
||||
file.read_exact(unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
(&raw mut descriptor).cast(),
|
||||
&mut descriptor as *mut _ as *mut u8,
|
||||
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
|
||||
file.read_exact(unsafe {
|
||||
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>(),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"]
|
||||
edition.workspace = true
|
||||
name = "block"
|
||||
rust-version.workspace = true
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
@@ -10,11 +9,10 @@ default = []
|
||||
io_uring = ["dep:io-uring"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
crc-any = "2.5.0"
|
||||
flate2 = "1.1"
|
||||
io-uring = { version = "0.7.12", optional = true }
|
||||
flate2 = "1.0"
|
||||
io-uring = { version = "0.7.11", optional = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
remain = "0.2.15"
|
||||
@@ -33,8 +31,5 @@ vm-virtio = { path = "../vm-virtio" }
|
||||
vmm-sys-util = { workspace = true }
|
||||
zstd = "0.13"
|
||||
|
||||
[dev-dependencies]
|
||||
cfg-if = { workspace = true }
|
||||
|
||||
[lints]
|
||||
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 vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::{BatchRequest, SECTOR_SIZE};
|
||||
use crate::{BatchRequest, DiskTopology};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DiskFileError {
|
||||
@@ -24,16 +24,14 @@ pub enum DiskFileError {
|
||||
/// Resize failed
|
||||
#[error("Resize failed")]
|
||||
ResizeError(#[source] std::io::Error),
|
||||
#[error("Failed cloning disk file")]
|
||||
Clone(#[source] std::io::Error),
|
||||
}
|
||||
|
||||
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
|
||||
/// 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.
|
||||
///
|
||||
/// [`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)]
|
||||
pub enum AsyncIoError {
|
||||
/// Failed vectored reading from file.
|
||||
@@ -69,12 +96,6 @@ pub enum AsyncIoError {
|
||||
/// Failed synchronizing file.
|
||||
#[error("Failed synchronizing file")]
|
||||
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.
|
||||
#[error("Failed submitting batch requests")]
|
||||
SubmitBatchRequests(#[source] std::io::Error),
|
||||
@@ -97,8 +118,6 @@ pub trait AsyncIo: Send {
|
||||
user_data: 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 batch_requests_enabled(&self) -> bool {
|
||||
false
|
||||
@@ -106,7 +125,4 @@ pub trait AsyncIo: Send {
|
||||
fn submit_batch_requests(&mut self, _batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
|
||||
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::io;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::str::FromStr;
|
||||
|
||||
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.
|
||||
const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> libc::flock {
|
||||
libc::flock {
|
||||
|
||||
@@ -2,14 +2,50 @@
|
||||
//
|
||||
// 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 crate::BatchRequest;
|
||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||
use crate::error::BlockResult;
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||
};
|
||||
use crate::fixed_vhd::FixedVhd;
|
||||
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 {
|
||||
raw_file_async: RawFileAsync,
|
||||
@@ -17,7 +53,7 @@ pub struct 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)?;
|
||||
|
||||
Ok(FixedVhdAsync {
|
||||
@@ -79,18 +115,6 @@ impl AsyncIo for FixedVhdAsync {
|
||||
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 {
|
||||
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
|
||||
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
raw_file_sync: RawFileSync,
|
||||
size: u64,
|
||||
@@ -73,16 +113,4 @@ impl AsyncIo for FixedVhdSync {
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
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
|
||||
|
||||
mod aligned_operation;
|
||||
pub mod async_io;
|
||||
pub mod disk_file;
|
||||
pub mod error;
|
||||
pub mod factory;
|
||||
pub mod fcntl;
|
||||
pub mod fixed_vhd;
|
||||
#[cfg(feature = "io_uring")]
|
||||
/// Enabled with the `"io_uring"` feature
|
||||
pub mod fixed_vhd_async;
|
||||
pub mod fixed_vhd_disk;
|
||||
pub mod fixed_vhd_sync;
|
||||
pub mod qcow;
|
||||
pub mod qcow_sync;
|
||||
#[cfg(feature = "io_uring")]
|
||||
pub(crate) mod qcow_async;
|
||||
pub(crate) mod qcow_common;
|
||||
pub mod qcow_disk;
|
||||
pub(crate) mod qcow_sync;
|
||||
#[cfg(feature = "io_uring")]
|
||||
pub(crate) mod raw_async;
|
||||
pub(crate) mod raw_async_aio;
|
||||
#[cfg(test)]
|
||||
mod raw_async_io_tests;
|
||||
pub mod raw_disk;
|
||||
pub(crate) mod raw_sync;
|
||||
mod request;
|
||||
/// Async primitives based on `io-uring`
|
||||
///
|
||||
/// Enabled with the `"io_uring"` feature
|
||||
pub mod raw_async;
|
||||
pub mod raw_async_aio;
|
||||
pub mod raw_sync;
|
||||
pub mod vhd;
|
||||
pub mod vhdx;
|
||||
pub mod vhdx_sync;
|
||||
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
use std::alloc::{Layout, alloc_zeroed, dealloc};
|
||||
use std::collections::VecDeque;
|
||||
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::os::linux::fs::MetadataExt;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
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")]
|
||||
use io_uring::{IoUring, Probe, opcode};
|
||||
use libc::{
|
||||
FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, S_IFBLK, S_IFMT, ioctl,
|
||||
};
|
||||
use log::{debug, info, warn};
|
||||
pub use request::{BatchRequest, ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType};
|
||||
use libc::{S_IFBLK, S_IFMT, ioctl};
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use thiserror::Error;
|
||||
use virtio_bindings::virtio_blk::*;
|
||||
use virtio_queue::DescriptorChain;
|
||||
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::{aio, ioctl_io_nr, ioctl_ior_nr};
|
||||
use vmm_sys_util::{aio, ioctl_io_nr};
|
||||
|
||||
use crate::async_io::{AsyncIoError, AsyncIoResult};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
||||
use crate::request::{DEFAULT_DESCRIPTOR_VEC_SIZE, SECTOR_SIZE};
|
||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||
use crate::vhdx::VhdxError;
|
||||
|
||||
const SECTOR_SHIFT: u8 = 9;
|
||||
pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Guest gave us bad memory addresses")]
|
||||
@@ -100,8 +91,6 @@ pub enum Error {
|
||||
RawFileError(#[source] std::io::Error),
|
||||
#[error("The requested operation does not support multiple descriptors")]
|
||||
TooManyDescriptors,
|
||||
#[error("Request contains too many segments ({0}, max {MAX_DISCARD_WRITE_ZEROES_SEG})")]
|
||||
TooManySegments(u32),
|
||||
#[error("Failure in vhdx")]
|
||||
VhdxError(#[source] VhdxError),
|
||||
}
|
||||
@@ -158,8 +147,6 @@ pub enum ExecuteError {
|
||||
WriteAll(#[source] io::Error),
|
||||
#[error("Unsupported request: {0}")]
|
||||
Unsupported(u32),
|
||||
#[error("Unsupported flags {flags:#x} for request type {request_type}")]
|
||||
UnsupportedFlags { request_type: u32, flags: u32 },
|
||||
#[error("Failed to submit io uring")]
|
||||
SubmitIoUring(#[source] io::Error),
|
||||
#[error("Failed to get guest address")]
|
||||
@@ -170,10 +157,6 @@ pub enum ExecuteError {
|
||||
AsyncWrite(#[source] AsyncIoError),
|
||||
#[error("failed to async flush")]
|
||||
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")]
|
||||
TemporaryBufferAllocation(#[source] io::Error),
|
||||
}
|
||||
@@ -190,20 +173,26 @@ impl ExecuteError {
|
||||
ExecuteError::Write(_) => VIRTIO_BLK_S_IOERR,
|
||||
ExecuteError::WriteAll(_) => VIRTIO_BLK_S_IOERR,
|
||||
ExecuteError::Unsupported(_) => VIRTIO_BLK_S_UNSUPP,
|
||||
ExecuteError::UnsupportedFlags { .. } => VIRTIO_BLK_S_UNSUPP,
|
||||
ExecuteError::SubmitIoUring(_) => VIRTIO_BLK_S_IOERR,
|
||||
ExecuteError::GetHostAddress(_) => VIRTIO_BLK_S_IOERR,
|
||||
ExecuteError::AsyncRead(_) => VIRTIO_BLK_S_IOERR,
|
||||
ExecuteError::AsyncWrite(_) => 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,
|
||||
};
|
||||
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>(
|
||||
mem: &vm_memory::GuestMemoryMmap<B>,
|
||||
desc_addr: GuestAddress,
|
||||
@@ -214,8 +203,6 @@ pub fn request_type<B: Bitmap + 'static>(
|
||||
VIRTIO_BLK_T_OUT => Ok(RequestType::Out),
|
||||
VIRTIO_BLK_T_FLUSH => Ok(RequestType::Flush),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
@@ -233,6 +220,372 @@ fn sector<B: Bitmap + 'static>(
|
||||
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)]
|
||||
#[repr(C, packed)]
|
||||
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 {
|
||||
fn read_vectored_sync(
|
||||
&mut self,
|
||||
@@ -616,27 +849,14 @@ pub fn read_aligned_block_size(f: &mut File) -> std::io::Result<Vec<u8>> {
|
||||
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.
|
||||
pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
|
||||
let block = read_aligned_block_size(f)
|
||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?;
|
||||
pub fn detect_image_type(f: &mut File) -> std::io::Result<ImageType> {
|
||||
let block = read_aligned_block_size(f)?;
|
||||
|
||||
// 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 {
|
||||
ImageType::Qcow2
|
||||
} else if vhd::is_fixed_vhd(f)
|
||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?
|
||||
{
|
||||
} else if vhd::is_fixed_vhd(f)? {
|
||||
ImageType::FixedVhd
|
||||
} else if u64::from_le_bytes(block[0..8].try_into().unwrap()) == VHDX_SIGN {
|
||||
ImageType::Vhdx
|
||||
@@ -681,36 +901,6 @@ ioctl_io_nr!(BLKSSZGET, 0x12, 104);
|
||||
ioctl_io_nr!(BLKPBSZGET, 0x12, 123);
|
||||
ioctl_io_nr!(BLKIOMIN, 0x12, 120);
|
||||
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)]
|
||||
enum BlockSize {
|
||||
@@ -757,73 +947,8 @@ impl DiskTopology {
|
||||
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> {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
|
||||
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 vmm_sys_util::write_zeroes::WriteZeroes;
|
||||
|
||||
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.
|
||||
#[derive(Debug)]
|
||||
pub struct QcowRawFile {
|
||||
file: RawFile,
|
||||
cluster_size: u64,
|
||||
cluster_mask: u64,
|
||||
refcount_block_entries: u64,
|
||||
read_refcount_fn: RefcountReader,
|
||||
write_refcount_fn: RefcountWriter,
|
||||
}
|
||||
|
||||
impl QcowRawFile {
|
||||
/// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not
|
||||
/// a power of two or refcount_bits is invalid.
|
||||
pub fn from(file: RawFile, cluster_size: u64, refcount_bits: u64) -> Option<Self> {
|
||||
/// a power of two.
|
||||
pub fn from(file: RawFile, cluster_size: u64) -> Option<Self> {
|
||||
if !cluster_size.is_power_of_two() {
|
||||
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 {
|
||||
file,
|
||||
cluster_size,
|
||||
cluster_mask: cluster_size - 1,
|
||||
refcount_block_entries,
|
||||
read_refcount_fn,
|
||||
write_refcount_fn,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -246,7 +86,7 @@ impl QcowRawFile {
|
||||
|
||||
for addr in entries {
|
||||
let entry = f(self, *addr)?;
|
||||
u64::write_be(&mut buffer, entry)?;
|
||||
buffer.write_u64::<BigEndian>(entry)?;
|
||||
}
|
||||
buffer.flush()?;
|
||||
Ok(())
|
||||
@@ -261,7 +101,7 @@ impl QcowRawFile {
|
||||
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
|
||||
|
||||
for &entry in entries {
|
||||
u64::write_be(&mut buffer, entry)?;
|
||||
buffer.write_u64::<BigEndian>(entry)?;
|
||||
}
|
||||
buffer.flush()?;
|
||||
Ok(())
|
||||
@@ -269,17 +109,24 @@ impl QcowRawFile {
|
||||
|
||||
/// Read a refcount block from the file and returns a Vec containing the block.
|
||||
/// Always returns a cluster's worth of data.
|
||||
#[inline]
|
||||
pub fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u64>> {
|
||||
pub fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u16>> {
|
||||
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.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.
|
||||
#[inline]
|
||||
pub fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> {
|
||||
pub fn write_refcount_block(&mut self, offset: u64, table: &[u16]) -> io::Result<()> {
|
||||
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.
|
||||
@@ -298,11 +145,6 @@ impl QcowRawFile {
|
||||
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.
|
||||
pub fn file_mut(&mut self) -> &mut RawFile {
|
||||
&mut self.file
|
||||
@@ -349,9 +191,6 @@ impl Clone for QcowRawFile {
|
||||
file: self.file.try_clone().expect("QcowRawFile cloning failed"),
|
||||
cluster_size: self.cluster_size,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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::fs::{File, Metadata};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
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::write_zeroes::{PunchHole, WriteZeroesAt};
|
||||
|
||||
use crate::{BlockBackend, query_device_size};
|
||||
use crate::BlockBackend;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawFile {
|
||||
@@ -38,7 +37,14 @@ fn is_valid_alignment(fd: RawFd, alignment: usize) -> bool {
|
||||
assert!(!ptr.is_null());
|
||||
|
||||
// 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
|
||||
unsafe { dealloc(ptr, layout) };
|
||||
@@ -116,21 +122,6 @@ impl RawFile {
|
||||
pub fn is_direct(&self) -> bool {
|
||||
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 {
|
||||
@@ -179,7 +170,7 @@ impl Read for RawFile {
|
||||
let ret = unsafe {
|
||||
::libc::pread64(
|
||||
self.file.as_raw_fd(),
|
||||
tmp_buf.as_mut_ptr().cast(),
|
||||
tmp_buf.as_mut_ptr() as *mut c_void,
|
||||
tmp_buf.len(),
|
||||
rounded_pos.try_into().unwrap(),
|
||||
)
|
||||
@@ -259,7 +250,7 @@ impl Write for RawFile {
|
||||
let ret = unsafe {
|
||||
::libc::pread64(
|
||||
self.file.as_raw_fd(),
|
||||
tmp_buf.as_mut_ptr().cast(),
|
||||
tmp_buf.as_mut_ptr() as *mut c_void,
|
||||
tmp_buf.len(),
|
||||
rounded_pos.try_into().unwrap(),
|
||||
)
|
||||
@@ -278,7 +269,7 @@ impl Write for RawFile {
|
||||
let ret = unsafe {
|
||||
::libc::pwrite64(
|
||||
self.file.as_raw_fd(),
|
||||
tmp_buf.as_ptr().cast(),
|
||||
tmp_buf.as_ptr() as *const c_void,
|
||||
tmp_buf.len(),
|
||||
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 {
|
||||
fn seek_hole(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
|
||||
match self.file.seek_hole(offset) {
|
||||
@@ -370,15 +355,11 @@ impl SeekHole for RawFile {
|
||||
|
||||
impl BlockBackend for RawFile {
|
||||
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
|
||||
Ok(query_device_size(&self.file)
|
||||
.map_err(crate::Error::RawFileError)?
|
||||
.0)
|
||||
Ok(self.metadata().map_err(crate::Error::RawFileError)?.len())
|
||||
}
|
||||
|
||||
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
|
||||
Ok(query_device_size(&self.file)
|
||||
.map_err(crate::Error::RawFileError)?
|
||||
.1)
|
||||
Ok(self.metadata().map_err(crate::Error::RawFileError)?.len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +379,3 @@ impl AsRawFd for RawFile {
|
||||
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.
|
||||
#[error("Address requested is not within the range of the disk")]
|
||||
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.
|
||||
#[error("Cluster with addr={0} needs to be read")]
|
||||
NeedCluster(u64),
|
||||
@@ -32,13 +29,6 @@ pub enum Error {
|
||||
/// `ReadingRefCounts` - Error reading the file into the refcount cache.
|
||||
#[error("Failed to read the file into the refcount cache")]
|
||||
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>;
|
||||
@@ -48,19 +38,16 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub struct RefCount {
|
||||
ref_table: VecCache<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.
|
||||
cluster_size: 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 {
|
||||
/// 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_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.
|
||||
pub fn new(
|
||||
raw_file: &mut QcowRawFile,
|
||||
@@ -68,7 +55,6 @@ impl RefCount {
|
||||
refcount_table_entries: u64,
|
||||
refcount_block_entries: u64,
|
||||
cluster_size: u64,
|
||||
refcount_bits: u64,
|
||||
) -> io::Result<RefCount> {
|
||||
let ref_table = VecCache::from_vec(raw_file.read_pointer_table(
|
||||
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_offset = max_valid_cluster_index * cluster_size;
|
||||
let max_refcount = if refcount_bits >= 64 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << refcount_bits) - 1
|
||||
};
|
||||
Ok(RefCount {
|
||||
ref_table,
|
||||
refcount_table_offset,
|
||||
@@ -89,8 +70,6 @@ impl RefCount {
|
||||
refcount_block_entries,
|
||||
cluster_size,
|
||||
max_valid_cluster_offset,
|
||||
max_refcount,
|
||||
refcount_bits,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,17 +92,9 @@ impl RefCount {
|
||||
&mut self,
|
||||
raw_file: &mut QcowRawFile,
|
||||
cluster_address: u64,
|
||||
refcount: u64,
|
||||
mut new_cluster: Option<(u64, VecCache<u64>)>,
|
||||
refcount: u16,
|
||||
mut new_cluster: Option<(u64, VecCache<u16>)>,
|
||||
) -> 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 block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||
@@ -199,15 +170,12 @@ impl RefCount {
|
||||
&mut self,
|
||||
raw_file: &mut QcowRawFile,
|
||||
address: u64,
|
||||
) -> Result<u64> {
|
||||
) -> Result<u16> {
|
||||
let (table_index, block_index) = self.get_refcount_index(address);
|
||||
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||
if block_addr_disk == 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) {
|
||||
let table = VecCache::from_vec(
|
||||
raw_file
|
||||
@@ -234,7 +202,7 @@ impl RefCount {
|
||||
&mut self,
|
||||
raw_file: &mut QcowRawFile,
|
||||
table_index: usize,
|
||||
) -> Result<Option<&[u64]>> {
|
||||
) -> Result<Option<&[u16]>> {
|
||||
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
|
||||
if block_addr_disk == 0 {
|
||||
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 {
|
||||
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> {
|
||||
|
||||
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
|
||||
|
||||
use std::io::Error;
|
||||
use std::fs::File;
|
||||
use std::io::{Error, Seek, SeekFrom};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
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 crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
||||
use crate::{BatchRequest, RequestType, SECTOR_SIZE};
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||
};
|
||||
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 {
|
||||
fd: RawFd,
|
||||
io_uring: IoUring,
|
||||
eventfd: EventFd,
|
||||
alignment: u64,
|
||||
}
|
||||
|
||||
impl RawFileAsync {
|
||||
pub fn new(fd: RawFd, ring_depth: u32) -> BlockResult<Self> {
|
||||
let io_uring =
|
||||
IoUring::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
||||
let eventfd =
|
||||
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
||||
pub fn new(fd: RawFd, ring_depth: u32) -> std::io::Result<Self> {
|
||||
let io_uring = IoUring::new(ring_depth)?;
|
||||
let eventfd = EventFd::new(libc::EFD_NONBLOCK)?;
|
||||
|
||||
// Register the io_uring eventfd that will notify when something in
|
||||
// the completion queue is ready.
|
||||
io_uring
|
||||
.submitter()
|
||||
.register_eventfd(eventfd.as_raw_fd())
|
||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
||||
io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?;
|
||||
|
||||
Ok(RawFileAsync {
|
||||
fd,
|
||||
io_uring,
|
||||
eventfd,
|
||||
alignment: SECTOR_SIZE,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,10 +92,6 @@ impl AsyncIo for RawFileAsync {
|
||||
&self.eventfd
|
||||
}
|
||||
|
||||
fn alignment(&self) -> u64 {
|
||||
self.alignment
|
||||
}
|
||||
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
@@ -69,9 +109,7 @@ impl AsyncIo for RawFileAsync {
|
||||
.build()
|
||||
.user_data(user_data),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AsyncIoError::ReadVectored(Error::other(format!("Submission queue is full: {e:?}")))
|
||||
})?;
|
||||
.map_err(|_| AsyncIoError::ReadVectored(Error::other("Submission queue is full")))?;
|
||||
};
|
||||
|
||||
// Update the submission queue and submit new operations to the
|
||||
@@ -99,11 +137,7 @@ impl AsyncIo for RawFileAsync {
|
||||
.build()
|
||||
.user_data(user_data),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AsyncIoError::WriteVectored(Error::other(format!(
|
||||
"Submission queue is full: {e:?}"
|
||||
)))
|
||||
})?;
|
||||
.map_err(|_| AsyncIoError::WriteVectored(Error::other("Submission queue is full")))?;
|
||||
};
|
||||
|
||||
// Update the submission queue and submit new operations to the
|
||||
@@ -125,9 +159,7 @@ impl AsyncIo for RawFileAsync {
|
||||
.build()
|
||||
.user_data(user_data),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AsyncIoError::Fsync(Error::other(format!("Submission queue is full: {e:?}")))
|
||||
})?;
|
||||
.map_err(|_| AsyncIoError::Fsync(Error::other("Submission queue is full")))?;
|
||||
};
|
||||
|
||||
// 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 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 {
|
||||
match req.request_type {
|
||||
RequestType::In => {
|
||||
@@ -185,10 +209,8 @@ impl AsyncIo for RawFileAsync {
|
||||
.build()
|
||||
.user_data(req.user_data),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AsyncIoError::ReadVectored(Error::other(format!(
|
||||
"Submission queue is full: {e:?}"
|
||||
)))
|
||||
.map_err(|_| {
|
||||
AsyncIoError::ReadVectored(Error::other("Submission queue is full"))
|
||||
})?;
|
||||
};
|
||||
submitted = true;
|
||||
@@ -207,10 +229,8 @@ impl AsyncIo for RawFileAsync {
|
||||
.build()
|
||||
.user_data(req.user_data),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AsyncIoError::WriteVectored(Error::other(format!(
|
||||
"Submission queue is full: {e:?}"
|
||||
)))
|
||||
.map_err(|_| {
|
||||
AsyncIoError::WriteVectored(Error::other("Submission queue is full"))
|
||||
})?;
|
||||
};
|
||||
submitted = true;
|
||||
@@ -233,54 +253,4 @@ impl AsyncIo for RawFileAsync {
|
||||
|
||||
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
|
||||
//
|
||||
|
||||
use std::collections::VecDeque;
|
||||
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::aio;
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::SECTOR_SIZE;
|
||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
||||
use crate::DiskTopology;
|
||||
use crate::async_io::{
|
||||
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 {
|
||||
fd: RawFd,
|
||||
ctx: aio::IoContext,
|
||||
eventfd: EventFd,
|
||||
alignment: u64,
|
||||
completion_list: VecDeque<(u64, i32)>,
|
||||
}
|
||||
|
||||
impl RawFileAsyncAio {
|
||||
pub fn new(fd: RawFd, queue_depth: u32) -> BlockResult<Self> {
|
||||
let eventfd =
|
||||
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
||||
let ctx =
|
||||
aio::IoContext::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
||||
pub fn new(fd: RawFd, queue_depth: u32) -> std::io::Result<Self> {
|
||||
let eventfd = EventFd::new(libc::EFD_NONBLOCK)?;
|
||||
let ctx = aio::IoContext::new(queue_depth)?;
|
||||
|
||||
Ok(RawFileAsyncAio {
|
||||
fd,
|
||||
ctx,
|
||||
eventfd,
|
||||
alignment: SECTOR_SIZE,
|
||||
completion_list: VecDeque::new(),
|
||||
})
|
||||
Ok(RawFileAsyncAio { fd, ctx, eventfd })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +83,6 @@ impl AsyncIo for RawFileAsyncAio {
|
||||
&self.eventfd
|
||||
}
|
||||
|
||||
fn alignment(&self) -> u64 {
|
||||
self.alignment
|
||||
}
|
||||
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
@@ -120,99 +153,12 @@ impl AsyncIo for RawFileAsyncAio {
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
if self.completion_list.is_empty() {
|
||||
// Drain pending AIO completions batched into the same queue.
|
||||
let mut events = [aio::IoEvent::default(); 32];
|
||||
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
|
||||
for event in &events[..rc] {
|
||||
self.completion_list
|
||||
.push_back((event.data, event.res as i32));
|
||||
}
|
||||
let mut events: [aio::IoEvent; 1] = [aio::IoEvent::default()];
|
||||
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
|
||||
if rc == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((events[0].data, events[0].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
|
||||
|
||||
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 crate::SECTOR_SIZE;
|
||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
|
||||
use crate::DiskTopology;
|
||||
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 {
|
||||
fd: RawFd,
|
||||
eventfd: EventFd,
|
||||
completion_list: VecDeque<(u64, i32)>,
|
||||
alignment: u64,
|
||||
}
|
||||
|
||||
impl RawFileSync {
|
||||
@@ -24,7 +69,6 @@ impl RawFileSync {
|
||||
fd,
|
||||
eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"),
|
||||
completion_list: VecDeque::new(),
|
||||
alignment: SECTOR_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +78,6 @@ impl AsyncIo for RawFileSync {
|
||||
&self.eventfd
|
||||
}
|
||||
|
||||
fn alignment(&self) -> u64 {
|
||||
self.alignment
|
||||
}
|
||||
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
@@ -106,82 +146,4 @@ impl AsyncIo for RawFileSync {
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
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)?;
|
||||
|
||||
// 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 {
|
||||
return Err(VhdxHeaderError::InvalidHeaderSign);
|
||||
}
|
||||
@@ -151,8 +151,9 @@ impl Header {
|
||||
/// Converts the header structure into a buffer
|
||||
fn write_to_buffer(&self, buffer: &mut [u8; HEADER_SIZE as usize]) {
|
||||
// SAFETY: self is a valid header.
|
||||
let reference =
|
||||
unsafe { std::slice::from_raw_parts((&raw const *self).cast(), HEADER_SIZE as usize) };
|
||||
let reference = unsafe {
|
||||
std::slice::from_raw_parts(self as *const Header as *const u8, HEADER_SIZE as usize)
|
||||
};
|
||||
*buffer = reference.try_into().unwrap();
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ impl RegionTableHeader {
|
||||
.map_err(VhdxHeaderError::ReadRegionTableHeader)?;
|
||||
|
||||
// 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 {
|
||||
return Err(VhdxHeaderError::InvalidRegionSign);
|
||||
}
|
||||
@@ -339,7 +340,7 @@ impl RegionTableEntry {
|
||||
pub fn new(buffer: &[u8]) -> Result<RegionTableEntry> {
|
||||
assert!(buffer.len() == std::mem::size_of::<RegionTableEntry>());
|
||||
// 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);
|
||||
region_table_entry.guid = uuid;
|
||||
|
||||
@@ -280,7 +280,7 @@ impl MetadataTableHeader {
|
||||
pub fn new(buffer: &[u8]) -> Result<MetadataTableHeader> {
|
||||
assert!(buffer.len() == std::mem::size_of::<MetadataTableHeader>());
|
||||
// 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 {
|
||||
return Err(VhdxMetadataError::InvalidMetadataSign);
|
||||
@@ -313,7 +313,7 @@ impl MetadataTableEntry {
|
||||
fn new(buffer: &[u8]) -> Result<MetadataTableEntry> {
|
||||
assert!(buffer.len() == std::mem::size_of::<MetadataTableEntry>());
|
||||
// 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);
|
||||
metadata_table_entry.item_id = uuid;
|
||||
|
||||
@@ -5,115 +5,67 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
||||
use crate::vhdx::{Vhdx, VhdxError};
|
||||
use crate::{AsyncAdaptor, BlockBackend, Error, disk_file};
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
|
||||
};
|
||||
use crate::vhdx::{Result as VhdxResult, Vhdx};
|
||||
use crate::{AsyncAdaptor, BlockBackend, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VhdxDiskSync {
|
||||
// FIXME: The Mutex serializes all VHDX I/O operations across queues, which
|
||||
// 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>>,
|
||||
vhdx_file: Vhdx,
|
||||
}
|
||||
|
||||
impl VhdxDiskSync {
|
||||
pub fn new(f: File) -> BlockResult<Self> {
|
||||
pub fn new(f: File) -> VhdxResult<Self> {
|
||||
Ok(VhdxDiskSync {
|
||||
vhdx_file: Arc::new(Mutex::new(Vhdx::new(f).map_err(|e| {
|
||||
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)
|
||||
})?)),
|
||||
vhdx_file: Vhdx::new(f)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskSize for VhdxDiskSync {
|
||||
fn logical_size(&self) -> BlockResult<u64> {
|
||||
Ok(self.vhdx_file.lock().unwrap().virtual_disk_size())
|
||||
impl DiskFile for VhdxDiskSync {
|
||||
fn logical_size(&mut self) -> DiskFileResult<u64> {
|
||||
Ok(self.vhdx_file.virtual_disk_size())
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::PhysicalSize for VhdxDiskSync {
|
||||
fn physical_size(&self) -> BlockResult<u64> {
|
||||
self.vhdx_file
|
||||
.lock()
|
||||
.unwrap()
|
||||
.physical_size()
|
||||
.map_err(|e| match e {
|
||||
Error::GetFileMetadata(io) => {
|
||||
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
|
||||
}
|
||||
_ => unreachable!("unexpected error from Vhdx::physical_size(): {e}"),
|
||||
})
|
||||
fn physical_size(&mut self) -> DiskFileResult<u64> {
|
||||
self.vhdx_file.physical_size().map_err(|e| {
|
||||
let io_inner = match e {
|
||||
Error::GetFileMetadata(e) => e,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
DiskFileError::Size(io_inner)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskFd for VhdxDiskSync {
|
||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
||||
BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd())
|
||||
}
|
||||
}
|
||||
|
||||
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")),
|
||||
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||
Ok(
|
||||
Box::new(VhdxSync::new(self.vhdx_file.clone()).map_err(DiskFileError::NewAsyncIo)?)
|
||||
as Box<dyn AsyncIo>,
|
||||
)
|
||||
.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>> {
|
||||
Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file))))
|
||||
fn fd(&mut self) -> BorrowedDiskFd<'_> {
|
||||
BorrowedDiskFd::new(self.vhdx_file.as_raw_fd())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VhdxSync {
|
||||
vhdx_file: Arc<Mutex<Vhdx>>,
|
||||
vhdx_file: Vhdx,
|
||||
eventfd: EventFd,
|
||||
completion_list: VecDeque<(u64, i32)>,
|
||||
}
|
||||
|
||||
impl VhdxSync {
|
||||
pub fn new(vhdx_file: Arc<Mutex<Vhdx>>) -> Self {
|
||||
VhdxSync {
|
||||
pub fn new(vhdx_file: Vhdx) -> std::io::Result<Self> {
|
||||
Ok(VhdxSync {
|
||||
vhdx_file,
|
||||
eventfd: EventFd::new(libc::EFD_NONBLOCK)
|
||||
.expect("Failed creating EventFd for VhdxSync"),
|
||||
eventfd: EventFd::new(libc::EFD_NONBLOCK)?,
|
||||
completion_list: VecDeque::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +82,7 @@ impl AsyncIo for VhdxSync {
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.vhdx_file.lock().unwrap().read_vectored_sync(
|
||||
self.vhdx_file.read_vectored_sync(
|
||||
offset,
|
||||
iovecs,
|
||||
user_data,
|
||||
@@ -145,7 +97,7 @@ impl AsyncIo for VhdxSync {
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.vhdx_file.lock().unwrap().write_vectored_sync(
|
||||
self.vhdx_file.write_vectored_sync(
|
||||
offset,
|
||||
iovecs,
|
||||
user_data,
|
||||
@@ -155,26 +107,11 @@ impl AsyncIo for VhdxSync {
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
self.vhdx_file.lock().unwrap().fsync_sync(
|
||||
user_data,
|
||||
&self.eventfd,
|
||||
&mut self.completion_list,
|
||||
)
|
||||
self.vhdx_file
|
||||
.fsync_sync(user_data, &self.eventfd, &mut self.completion_list)
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
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"
|
||||
license = "Apache-2.0 AND BSD-3-Clause"
|
||||
name = "cloud-hypervisor"
|
||||
rust-version.workspace = true
|
||||
version = "52.0.0"
|
||||
version = "50.2.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]
|
||||
anyhow = { workspace = true }
|
||||
@@ -19,7 +26,6 @@ env_logger = { workspace = true }
|
||||
epoll = { workspace = true }
|
||||
event_monitor = { path = "../event_monitor" }
|
||||
hypervisor = { path = "../hypervisor" }
|
||||
jiff = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true, features = ["std"] }
|
||||
option_parser = { path = "../option_parser" }
|
||||
@@ -32,7 +38,7 @@ tracer = { path = "../tracer" }
|
||||
vm-memory = { workspace = true }
|
||||
vmm = { path = "../vmm" }
|
||||
vmm-sys-util = { workspace = true }
|
||||
zbus = { version = "5.15.0", optional = true }
|
||||
zbus = { version = "5.7.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
block = { path = "../block" }
|
||||
|
||||
@@ -16,16 +16,14 @@ use api_client::{
|
||||
Error as ApiClientError, simple_api_command, simple_api_command_with_fds,
|
||||
simple_api_full_command,
|
||||
};
|
||||
#[cfg(feature = "dbus_api")]
|
||||
use clap::ArgAction;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use log::error;
|
||||
use option_parser::{ByteSized, ByteSizedParseError};
|
||||
use thiserror::Error;
|
||||
use vmm::config::RestoreConfig;
|
||||
use vmm::vm_config::{
|
||||
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
|
||||
UserDeviceConfig, VdpaConfig, VsockConfig,
|
||||
DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig,
|
||||
VsockConfig,
|
||||
};
|
||||
#[cfg(feature = "dbus_api")]
|
||||
use zbus::{proxy, zvariant::Optional};
|
||||
@@ -51,8 +49,6 @@ enum Error {
|
||||
AddDiskConfig(#[source] vmm::config::Error),
|
||||
#[error("Error parsing filesystem syntax")]
|
||||
AddFsConfig(#[source] vmm::config::Error),
|
||||
#[error("Error parsing generic vhost-user syntax")]
|
||||
AddGenericVhostUserConfig(#[source] vmm::config::Error),
|
||||
#[error("Error parsing persistent memory syntax")]
|
||||
AddPmemConfig(#[source] vmm::config::Error),
|
||||
#[error("Error parsing network syntax")]
|
||||
@@ -71,8 +67,6 @@ enum Error {
|
||||
ReadingFile(#[source] std::io::Error),
|
||||
#[error("Invalid disk size")]
|
||||
InvalidDiskSize(#[source] ByteSizedParseError),
|
||||
#[error("Error parsing send migration configuration")]
|
||||
SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError),
|
||||
}
|
||||
|
||||
enum TargetApi<'a> {
|
||||
@@ -89,10 +83,6 @@ trait DBusApi1 {
|
||||
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_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_pmem(&self, pmem_config: &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))
|
||||
}
|
||||
|
||||
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 {
|
||||
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))
|
||||
.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") => {
|
||||
let pmem_config = add_pmem_config(
|
||||
matches
|
||||
@@ -523,7 +493,11 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu
|
||||
.unwrap()
|
||||
.get_one::<String>("send_migration_config")
|
||||
.unwrap(),
|
||||
)?;
|
||||
matches
|
||||
.subcommand_matches("send-migration")
|
||||
.unwrap()
|
||||
.get_flag("send_migration_local"),
|
||||
);
|
||||
simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data))
|
||||
.map_err(Error::HttpApiClient)
|
||||
}
|
||||
@@ -646,16 +620,6 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
|
||||
)?;
|
||||
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") => {
|
||||
let pmem_config = add_pmem_config(
|
||||
matches
|
||||
@@ -743,7 +707,11 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
|
||||
.unwrap()
|
||||
.get_one::<String>("send_migration_config")
|
||||
.unwrap(),
|
||||
)?;
|
||||
matches
|
||||
.subcommand_matches("send-migration")
|
||||
.unwrap()
|
||||
.get_flag("send_migration_local"),
|
||||
);
|
||||
proxy.api_vm_send_migration(&send_migration_data)
|
||||
}
|
||||
Some("receive-migration") => {
|
||||
@@ -867,14 +835,6 @@ fn add_fs_config(config: &str) -> Result<String, Error> {
|
||||
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> {
|
||||
let pmem_config = PmemConfig::parse(config).map_err(Error::AddPmemConfig)?;
|
||||
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()
|
||||
}
|
||||
|
||||
fn send_migration_data(config: &str) -> Result<String, Error> {
|
||||
let send_migration_data =
|
||||
vmm::api::VmSendMigrationData::parse(config).map_err(Error::SendMigrationConfig)?;
|
||||
let send_migration_config = serde_json::to_string(&send_migration_data).unwrap();
|
||||
Ok(send_migration_config)
|
||||
fn send_migration_data(url: &str, local: bool) -> String {
|
||||
let send_migration_data = vmm::api::VmSendMigrationData {
|
||||
destination_url: url.to_owned(),
|
||||
local,
|
||||
};
|
||||
|
||||
serde_json::to_string(&send_migration_data).unwrap()
|
||||
}
|
||||
|
||||
fn create_data(path: &str) -> Result<String, Error> {
|
||||
@@ -1019,13 +981,6 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
|
||||
.index(1)
|
||||
.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")
|
||||
.about("Add network device")
|
||||
.arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)),
|
||||
@@ -1135,7 +1090,13 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
|
||||
.arg(
|
||||
Arg::new("send_migration_config")
|
||||
.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-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
|
||||
//
|
||||
|
||||
mod logger;
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::{env, io};
|
||||
|
||||
@@ -32,16 +32,13 @@ use vmm::vm_config::FwCfgConfig;
|
||||
#[cfg(feature = "ivshmem")]
|
||||
use vmm::vm_config::IvshmemConfig;
|
||||
use vmm::vm_config::{
|
||||
BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig,
|
||||
LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig,
|
||||
RateLimiterGroupConfig, RngConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig,
|
||||
BalloonConfig, DeviceConfig, DiskConfig, FsConfig, LandlockConfig, NetConfig, NumaConfig,
|
||||
PciSegmentConfig, PmemConfig, RateLimiterGroupConfig, TpmConfig, UserDeviceConfig, VdpaConfig,
|
||||
VmConfig, VsockConfig,
|
||||
};
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
use vmm_sys_util::signal::block_signal;
|
||||
|
||||
use crate::logger::Logger;
|
||||
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
#[global_allocator]
|
||||
static ALLOC: dhat::Alloc = dhat::Alloc;
|
||||
@@ -97,8 +94,6 @@ enum Error {
|
||||
BareGdb,
|
||||
#[error("Error creating log file")]
|
||||
LogFileCreation(#[source] std::io::Error),
|
||||
#[error("Error parsing logger format")]
|
||||
LoggerFormat(#[source] logger::Error),
|
||||
#[error("Error setting up logger")]
|
||||
LoggerSetup(#[source] log::SetLoggerError),
|
||||
#[error("Failed to gracefully shutdown http api")]
|
||||
@@ -121,6 +116,47 @@ enum FdTableError {
|
||||
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) {
|
||||
(default_vcpus(), default_memory(), default_rng())
|
||||
}
|
||||
@@ -138,7 +174,7 @@ fn default_memory() -> 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
|
||||
@@ -151,7 +187,7 @@ fn get_cli_options_sorted(
|
||||
[
|
||||
Arg::new("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)
|
||||
.group("vmm-config"),
|
||||
Arg::new("balloon")
|
||||
@@ -163,10 +199,11 @@ fn get_cli_options_sorted(
|
||||
.long("cmdline")
|
||||
.help("Kernel command line")
|
||||
.num_args(1)
|
||||
.group("vm-config"),
|
||||
Arg::new("console")
|
||||
.group("vm-config"), Arg::new("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")
|
||||
.group("vm-config"),
|
||||
Arg::new("cpus")
|
||||
@@ -177,16 +214,16 @@ fn get_cli_options_sorted(
|
||||
kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,\
|
||||
affinity=<list_of_vcpus_with_their_associated_cpuset>,\
|
||||
features=<list_of_features_to_enable>,\
|
||||
nested=on|off,core_scheduling=vm|vcpu|off",
|
||||
nested=on|off",
|
||||
)
|
||||
.default_value(default_vcpus)
|
||||
.group("vm-config"),
|
||||
#[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(target_arch = "x86_64")]
|
||||
Arg::new("debug-console")
|
||||
.long("debug-console")
|
||||
.help("Debug console: off|pty|tty|file=</path/to/a/file>,iobase=<port in hex>")
|
||||
.default_value("off,iobase=0xe9")
|
||||
.group("vm-config"),
|
||||
#[cfg(feature = "dbus_api")]
|
||||
Arg::new("dbus-service-name")
|
||||
.long("dbus-service-name")
|
||||
@@ -194,33 +231,31 @@ fn get_cli_options_sorted(
|
||||
.num_args(1)
|
||||
.group("vmm-config"),
|
||||
#[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")
|
||||
.long("dbus-system-bus")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Use the system bus instead of a session bus")
|
||||
.num_args(0)
|
||||
.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")
|
||||
.long("device")
|
||||
.help(DeviceConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("disk")
|
||||
.long("disk")
|
||||
.help(DiskConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("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)
|
||||
.group("vmm-config"),
|
||||
Arg::new("firmware")
|
||||
@@ -232,7 +267,6 @@ fn get_cli_options_sorted(
|
||||
.long("fs")
|
||||
.help(FsConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
#[cfg(feature = "fw_cfg")]
|
||||
Arg::new("fw-cfg-config")
|
||||
@@ -243,15 +277,9 @@ fn get_cli_options_sorted(
|
||||
#[cfg(feature = "guest_debug")]
|
||||
Arg::new("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)
|
||||
.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")]
|
||||
Arg::new("igvm")
|
||||
.long("igvm")
|
||||
@@ -286,7 +314,9 @@ fn get_cli_options_sorted(
|
||||
Arg::new("landlock")
|
||||
.long("landlock")
|
||||
.num_args(0)
|
||||
.help("enable/disable Landlock.")
|
||||
.help(
|
||||
"enable/disable Landlock.",
|
||||
)
|
||||
.action(ArgAction::SetTrue)
|
||||
.default_value("false")
|
||||
.group("vm-config"),
|
||||
@@ -294,19 +324,12 @@ fn get_cli_options_sorted(
|
||||
.long("landlock-rules")
|
||||
.help(LandlockConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("log-file")
|
||||
.long("log-file")
|
||||
.help("Log file. Standard error is used if not specified")
|
||||
.num_args(1)
|
||||
.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")
|
||||
.long("memory")
|
||||
.help(
|
||||
@@ -333,42 +356,33 @@ fn get_cli_options_sorted(
|
||||
prefault=on|off\"",
|
||||
)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("net")
|
||||
.long("net")
|
||||
.help(NetConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.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")
|
||||
.long("numa")
|
||||
.help(NumaConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("pci-segment")
|
||||
.long("pci-segment")
|
||||
.help(PciSegmentConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("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)
|
||||
.group("vm-config"),
|
||||
Arg::new("pmem")
|
||||
.long("pmem")
|
||||
.help(PmemConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
#[cfg(feature = "pvmemcontrol")]
|
||||
Arg::new("pvmemcontrol")
|
||||
@@ -387,7 +401,6 @@ fn get_cli_options_sorted(
|
||||
.long("rate-limit-group")
|
||||
.help(RateLimiterGroupConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("restore")
|
||||
.long("restore")
|
||||
@@ -396,7 +409,9 @@ fn get_cli_options_sorted(
|
||||
.group("vmm-config"),
|
||||
Arg::new("rng")
|
||||
.long("rng")
|
||||
.help(RngConfig::SYNTAX)
|
||||
.help(
|
||||
"Random number generator parameters \"src=<entropy_source_path>,iommu=on|off\"",
|
||||
)
|
||||
.default_value(default_rng)
|
||||
.group("vm-config"),
|
||||
Arg::new("seccomp")
|
||||
@@ -406,7 +421,7 @@ fn get_cli_options_sorted(
|
||||
.default_value("true"),
|
||||
Arg::new("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")
|
||||
.group("vm-config"),
|
||||
Arg::new("tpm")
|
||||
@@ -418,7 +433,6 @@ fn get_cli_options_sorted(
|
||||
.long("user-device")
|
||||
.help(UserDeviceConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("v")
|
||||
.short('v')
|
||||
@@ -429,7 +443,6 @@ fn get_cli_options_sorted(
|
||||
.long("vdpa")
|
||||
.help(VdpaConfig::SYNTAX)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.group("vm-config"),
|
||||
Arg::new("version")
|
||||
.short('V')
|
||||
@@ -448,9 +461,7 @@ fn get_cli_options_sorted(
|
||||
.num_args(0)
|
||||
.action(ArgAction::SetTrue)
|
||||
.group("vm-config"),
|
||||
]
|
||||
.to_vec()
|
||||
.into_boxed_slice()
|
||||
].to_vec().into_boxed_slice()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
fn parse_api_socket(cmd_arguments: &ArgMatches) -> Result<(Option<String>, Option<RawFd>), 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> {
|
||||
fn start_vmm(cmd_arguments: &ArgMatches) -> Result<Option<String>, Error> {
|
||||
let log_level = match cmd_arguments.get_count("v") {
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
@@ -522,11 +503,37 @@ fn start_vmm(
|
||||
Box::new(std::io::stderr())
|
||||
};
|
||||
|
||||
let format = cmd_arguments.get_one::<String>("log-format").unwrap();
|
||||
let logger = Logger::new(log_file, format).map_err(Error::LoggerFormat)?;
|
||||
log::set_boxed_logger(Box::new(logger))
|
||||
.map(|()| log::set_max_level(log_level))
|
||||
.map_err(Error::LoggerSetup)?;
|
||||
log::set_boxed_logger(Box::new(Logger {
|
||||
output: Mutex::new(log_file),
|
||||
start: std::time::Instant::now(),
|
||||
}))
|
||||
.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_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)?;
|
||||
|
||||
#[cfg(feature = "guest_debug")]
|
||||
@@ -608,7 +613,6 @@ fn start_vmm(
|
||||
|
||||
let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?;
|
||||
let landlock_enable = cmd_arguments.get_flag("landlock");
|
||||
let no_shutdown = cmd_arguments.get_flag("no-shutdown");
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut event_monitor = cmd_arguments
|
||||
@@ -689,7 +693,7 @@ fn start_vmm(
|
||||
|
||||
let vmm_thread_handle = vmm::start_vmm_thread(
|
||||
vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")),
|
||||
api_socket_path,
|
||||
&api_socket_path,
|
||||
api_socket_fd,
|
||||
#[cfg(feature = "dbus_api")]
|
||||
dbus_options,
|
||||
@@ -705,7 +709,6 @@ fn start_vmm(
|
||||
exit_evt.try_clone().unwrap(),
|
||||
&seccomp_action,
|
||||
hypervisor,
|
||||
no_shutdown,
|
||||
landlock_enable,
|
||||
)
|
||||
.map_err(Error::StartVmmThread)?;
|
||||
@@ -776,7 +779,7 @@ fn start_vmm(
|
||||
dbus_api_graceful_shutdown(chs);
|
||||
}
|
||||
|
||||
r
|
||||
r.map(|_| api_socket_path)
|
||||
}
|
||||
|
||||
// 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}");
|
||||
}
|
||||
|
||||
let (api_socket_path, api_socket_fd) = match parse_api_socket(&cmd_arguments) {
|
||||
Ok(p) => p,
|
||||
Err(top_error) => {
|
||||
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(()) => {
|
||||
let exit_code = match start_vmm(&cmd_arguments) {
|
||||
Ok(path) => {
|
||||
path.map(|s| std::fs::remove_file(s).ok());
|
||||
info!("Cloud Hypervisor exited successfully");
|
||||
0
|
||||
}
|
||||
@@ -921,9 +911,8 @@ mod unit_tests {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use vmm::vm_config::DebugConsoleConfig;
|
||||
use vmm::vm_config::{
|
||||
CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures,
|
||||
CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig,
|
||||
SerialConfig, VmConfig,
|
||||
ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig,
|
||||
PayloadConfig, RngConfig, VmConfig,
|
||||
};
|
||||
|
||||
use crate::test_util::assert_args_sorted;
|
||||
@@ -974,7 +963,6 @@ mod unit_tests {
|
||||
affinity: None,
|
||||
features: CpuFeatures::default(),
|
||||
nested: true,
|
||||
core_scheduling: CoreScheduling::Vm,
|
||||
},
|
||||
memory: MemoryConfig {
|
||||
size: 536_870_912,
|
||||
@@ -1006,26 +994,22 @@ mod unit_tests {
|
||||
net: None,
|
||||
rng: RngConfig {
|
||||
src: PathBuf::from("/dev/urandom"),
|
||||
pci_common: PciDeviceCommonConfig::default(),
|
||||
iommu: false,
|
||||
},
|
||||
balloon: None,
|
||||
fs: None,
|
||||
generic_vhost_user: None,
|
||||
pmem: None,
|
||||
serial: SerialConfig {
|
||||
common: CommonConsoleConfig {
|
||||
file: None,
|
||||
mode: ConsoleOutputMode::Null,
|
||||
socket: None,
|
||||
},
|
||||
serial: ConsoleConfig {
|
||||
file: None,
|
||||
mode: ConsoleOutputMode::Null,
|
||||
iommu: false,
|
||||
socket: None,
|
||||
},
|
||||
console: ConsoleConfig {
|
||||
common: CommonConsoleConfig {
|
||||
file: None,
|
||||
mode: ConsoleOutputMode::Tty,
|
||||
socket: None,
|
||||
},
|
||||
pci_common: PciDeviceCommonConfig::default(),
|
||||
file: None,
|
||||
mode: ConsoleOutputMode::Tty,
|
||||
iommu: false,
|
||||
socket: None,
|
||||
},
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
debug_console: DebugConsoleConfig::default(),
|
||||
@@ -1693,12 +1677,10 @@ mod unit_tests {
|
||||
"--serial",
|
||||
"null",
|
||||
"--console",
|
||||
"tty,pci_segment=1,pci_device_id=7",
|
||||
"tty",
|
||||
],
|
||||
r#"{
|
||||
"payload": {"kernel": "/path/to/kernel"},
|
||||
"serial": {"mode": "Null"},
|
||||
"console": {"mode": "Tty", "iommu": false, "pci_segment": 1, "pci_device_id": 7}
|
||||
"payload": {"kernel": "/path/to/kernel"}
|
||||
}"#,
|
||||
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"]
|
||||
edition.workspace = true
|
||||
name = "devices"
|
||||
rust-version.workspace = true
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
acpi_tables = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
arch = { path = "../arch" }
|
||||
bitfield-struct = { version = "0.13.0", optional = true }
|
||||
bitfield-struct = { version = "0.12.0", optional = true }
|
||||
bitflags = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
event_monitor = { path = "../event_monitor" }
|
||||
@@ -21,7 +20,7 @@ linux-loader = { workspace = true, features = [
|
||||
"pe",
|
||||
], optional = true }
|
||||
log = { workspace = true }
|
||||
num_enum = "0.7.6"
|
||||
num_enum = "0.7.5"
|
||||
pci = { path = "../pci" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
@@ -35,7 +34,7 @@ vm-memory = { workspace = true, features = [
|
||||
] }
|
||||
vm-migration = { path = "../vm-migration" }
|
||||
vmm-sys-util = { workspace = true }
|
||||
zerocopy = { version = "0.8.48", features = [
|
||||
zerocopy = { version = "0.8.31", features = [
|
||||
"alloc",
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
@@ -21,7 +21,7 @@ pub const GED_DEVICE_ACPI_SIZE: usize = 0x1;
|
||||
|
||||
/// A device for handling ACPI shutdown and reboot
|
||||
pub struct AcpiShutdownDevice {
|
||||
guest_exit_evt: EventFd,
|
||||
exit_evt: EventFd,
|
||||
reset_evt: EventFd,
|
||||
vcpus_kill_signalled: Arc<AtomicBool>,
|
||||
}
|
||||
@@ -29,12 +29,12 @@ pub struct AcpiShutdownDevice {
|
||||
impl AcpiShutdownDevice {
|
||||
/// Constructs a device that will signal the given event when the guest requests it.
|
||||
pub fn new(
|
||||
guest_exit_evt: EventFd,
|
||||
exit_evt: EventFd,
|
||||
reset_evt: EventFd,
|
||||
vcpus_kill_signalled: Arc<AtomicBool>,
|
||||
) -> AcpiShutdownDevice {
|
||||
AcpiShutdownDevice {
|
||||
guest_exit_evt,
|
||||
exit_evt,
|
||||
reset_evt,
|
||||
vcpus_kill_signalled,
|
||||
}
|
||||
@@ -68,7 +68,7 @@ impl BusDevice for AcpiShutdownDevice {
|
||||
const SLEEP_VALUE_BIT: u8 = 2;
|
||||
if data[0] == (S5_SLEEP_VALUE << SLEEP_VALUE_BIT) | (1 << SLEEP_STATUS_EN_BIT) {
|
||||
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}");
|
||||
}
|
||||
// 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 {
|
||||
fn allocate_bars(
|
||||
&mut self,
|
||||
_allocator: &mut SystemAllocator,
|
||||
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||
mmio32_allocator: &mut AddressAllocator,
|
||||
mmio64_allocator: &mut AddressAllocator,
|
||||
resources: Option<Vec<Resource>>,
|
||||
@@ -382,10 +382,6 @@ impl PciDevice for IvshmemDevice {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) {
|
||||
self.configuration.restore_bar_addr(params);
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -122,13 +122,13 @@ impl BusDevice for Cmos {
|
||||
// the tm and timespec struct because it contains only plain data.
|
||||
let update_in_progress = unsafe {
|
||||
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
|
||||
#[cfg_attr(target_env = "musl", allow(deprecated))]
|
||||
let now: time_t = timespec.tv_sec;
|
||||
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.
|
||||
seconds = tm.tm_sec;
|
||||
|
||||
@@ -441,17 +441,12 @@ impl FwCfg {
|
||||
initramfs: Option<File>,
|
||||
cmdline: Option<std::ffi::CString>,
|
||||
fw_cfg_item_list: Option<Vec<FwCfgItem>>,
|
||||
#[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool,
|
||||
) -> Result<()> {
|
||||
if let Some(mem_size) = mem_size {
|
||||
self.add_e820(mem_size)?;
|
||||
}
|
||||
if let Some(kernel) = kernel {
|
||||
self.add_kernel_data(
|
||||
&kernel,
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
kvm_sev_snp_enabled,
|
||||
)?;
|
||||
self.add_kernel_data(&kernel)?;
|
||||
}
|
||||
if let Some(cmdline) = cmdline {
|
||||
self.add_kernel_cmdline(cmdline);
|
||||
@@ -636,49 +631,24 @@ impl FwCfg {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_kernel_data(
|
||||
&mut self,
|
||||
file: &File,
|
||||
#[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool,
|
||||
) -> Result<()> {
|
||||
pub fn add_kernel_data(&mut self, file: &File) -> Result<()> {
|
||||
let mut buffer = vec![0u8; size_of::<boot_params>()];
|
||||
file.read_exact_at(&mut buffer, 0)?;
|
||||
let bp = boot_params::from_mut_slice(&mut buffer).unwrap();
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
// For SEV-SNP guests on KVM, don't modify the kernel header so the
|
||||
// bytes sent via fw_cfg match what the VMM hashes for the launch digest.
|
||||
// The guest firmware handles these fields itself.
|
||||
if !kvm_sev_snp_enabled {
|
||||
if bp.hdr.setup_sects == 0 {
|
||||
bp.hdr.setup_sects = 4;
|
||||
}
|
||||
bp.hdr.type_of_loader = 0xff;
|
||||
// must set to 4 for backwards compatibility
|
||||
// https://docs.kernel.org/arch/x86/boot.html#the-real-mode-kernel-header
|
||||
if bp.hdr.setup_sects == 0 {
|
||||
bp.hdr.setup_sects = 4;
|
||||
}
|
||||
// wildcard boot loader type
|
||||
bp.hdr.type_of_loader = 0xff;
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
let kernel_start = bp.text_offset;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let kernel_start = {
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
|
||||
let kernel_start = (bp.hdr.setup_sects as usize + 1) * 512;
|
||||
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_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]
|
||||
fn test_dma() {
|
||||
let code = [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use std::sync::{Arc, Barrier, RwLock};
|
||||
use std::sync::{Arc, Barrier, Mutex, RwLock};
|
||||
use std::{io, result};
|
||||
|
||||
use log::{debug, warn};
|
||||
@@ -443,7 +443,7 @@ impl PvmemcontrolBusDevice {
|
||||
)));
|
||||
};
|
||||
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 {
|
||||
return Err(Error::LibcFail(io::Error::last_os_error()));
|
||||
}
|
||||
@@ -712,10 +712,6 @@ impl PciDevice for PvmemcontrolPciDevice {
|
||||
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 {
|
||||
self
|
||||
}
|
||||
@@ -726,7 +722,7 @@ impl PciDevice for PvmemcontrolPciDevice {
|
||||
|
||||
fn allocate_bars(
|
||||
&mut self,
|
||||
_allocator: &mut SystemAllocator,
|
||||
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||
mmio32_allocator: &mut AddressAllocator,
|
||||
_mmio64_allocator: &mut AddressAllocator,
|
||||
resources: Option<Vec<Resource>>,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::any::Any;
|
||||
use std::result;
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::sync::{Arc, Barrier, Mutex};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use event_monitor::event;
|
||||
@@ -174,7 +174,7 @@ impl PciDevice for PvPanicDevice {
|
||||
|
||||
fn allocate_bars(
|
||||
&mut self,
|
||||
_allocator: &mut SystemAllocator,
|
||||
_allocator: &Arc<Mutex<SystemAllocator>>,
|
||||
mmio32_allocator: &mut AddressAllocator,
|
||||
_mmio64_allocator: &mut AddressAllocator,
|
||||
resources: Option<Vec<Resource>>,
|
||||
@@ -231,10 +231,6 @@ impl PciDevice for PvPanicDevice {
|
||||
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]) {
|
||||
data[0] = self.events;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### 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
|
||||
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
|
||||
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.
|
||||
|
||||
## 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
|
||||
`sev_snp` feature enabled:
|
||||
@@ -25,7 +26,7 @@ cargo build --no-default-features --features "sev_snp"
|
||||
```
|
||||
|
||||
**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:
|
||||
|
||||
@@ -37,4 +38,4 @@ You can run a SEV-SNP VM using the following command:
|
||||
--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)
|
||||
- [Create a Virtual Machine](#create-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)
|
||||
- [Shut a Virtual Machine Down](#shut-a-virtual-machine-down)
|
||||
- [D-Bus API](#d-bus-api)
|
||||
- [D-Bus API Location and availability](#d-bus-api-location-and-availability)
|
||||
- [D-Bus API Interface](#d-bus-api-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)
|
||||
- [Goals and Design](#goals-and-design)
|
||||
- [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/)
|
||||
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,
|
||||
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
|
||||
|
||||
| Action | Endpoint | Request Body | Response Body | Prerequisites |
|
||||
| --------------------------------------- | ---------------------------- | --------------------------------- | ------------------------ | ------------------------------------------------------ |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Take 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 |
|
||||
| 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 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 |
|
||||
| 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 |
|
||||
| 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 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 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 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Start to send migration to target | `/vm.send-migration` | `/schemas/SendMigrationData` | N/A | The VM is booted and (shared mem or hugepages enabled) |
|
||||
| ---------------------------------- | ----------------------- | ------------------------------- | ------------------------ | ------------------------------------------------------ |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 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 |
|
||||
| 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 |
|
||||
| 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 fs device to the VM | `/vm.add-fs` | `/schemas/FsConfig` | `/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 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 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 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
* The `vmcoredump` action is available exclusively for the `x86_64`
|
||||
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'
|
||||
```
|
||||
|
||||
##### 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
|
||||
#!/usr/bin/env bash
|
||||
@@ -202,7 +201,7 @@ see [D-Bus API Interface](#d-bus-api-interface).
|
||||
#### D-Bus API Location and availability
|
||||
|
||||
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.
|
||||
|
||||
```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 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.
|
||||
|
||||
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)
|
||||
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
|
||||
[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
|
||||
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/),
|
||||
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:
|
||||
|
||||
1. A user or operator sends an HTTP request to the Cloud Hypervisor
|
||||
[REST API](#rest-api) in order to create a virtual machine:
|
||||
```shell
|
||||
[REST API](#rest-api) in order to creates a virtual machine:
|
||||
```
|
||||
shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
[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
|
||||
sender (The HTTP thread):
|
||||
sender ((The HTTP thread):
|
||||
```Rust
|
||||
match api_request {
|
||||
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