From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id BF7B21FF0BC for ; Thu, 15 Oct 2026 14:19:31 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 0D51321432; Thu, 15 Oct 2026 14:19:29 +0200 (CEST) Date: Thu, 15 Oct 2026 14:16:44 +0200 From: Wolfgang Bumiller To: Dominik Csapak Subject: Re: [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation Message-ID: References: <20261015081041.743422-1-d.csapak@proxmox.com> <20261015081041.743422-2-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20261015081041.743422-2-d.csapak@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1792066604766 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.481 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_SHORT 0.001 Use of a URL Shortener for very short URL RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record X-MailFrom: w.bumiller@proxmox.com X-Mailman-Rule-Hits: max-size X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; news-moderation; no-subject; digests; suspicious-header Message-ID-Hash: 5OYXA5LAMDVZXZUBGUCIU56L6TW5AQQC X-Message-ID-Hash: 5OYXA5LAMDVZXZUBGUCIU56L6TW5AQQC X-Mailman-Approved-At: Thu, 15 Oct 2026 14:18:56 +0200 CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Thu, Oct 15, 2026 at 10:10:23AM +0200, Dominik Csapak wrote: > Since the hardware layout, including PCI addresses, are part of the > guest's ABI, the assignment has to stay stable for the whole lifetime > of a VM. The Perl implementation in PVE::QemuServer::PCI encodes this as > two flat hashes of hardcoded addresses, which is hard to extend without > accidentally shifting an existing entry. > > Describe the assignment as a layout instead: a tree of buses, slots and > functions that says what may sit where, which addresses are reserved for > something outside of our control, and which are still free. Looking up a > device then means walking that tree, and adding a new device means > filling in a free slot rather than picking a number by hand. > > The layouts added here reproduce the current Perl address maps exactly, > including the quirks for legacy IGD passthrough, virtio-scsi-single and > the Windows 7 PCIe workaround. The tests cover every id the Perl maps > contain (except win7's hostpciXbus0, since that will change in perl > anyway). > > Lookups take a `HardwareConfig`, which only contains the parts of the > guest config that influence the layout and its quirks, so callers can > build it from whatever config representation they have. > > Also adds the cargo workspace and the build tooling the other Proxmox > Rust repositories use to build Debian packages. > > Signed-off-by: Dominik Csapak > --- > .cargo/config.toml | 5 + > .gitignore | 13 + > Cargo.toml | 29 + > Makefile | 153 +++++ > build.sh | 40 ++ > bump.sh | 44 ++ > pve-qemu-server-pci/Cargo.toml | 15 + > pve-qemu-server-pci/debian/changelog | 5 + > pve-qemu-server-pci/debian/control | 36 ++ > pve-qemu-server-pci/debian/copyright | 18 + > pve-qemu-server-pci/debian/debcargo.toml | 7 + > pve-qemu-server-pci/src/constants.rs | 7 + > pve-qemu-server-pci/src/error.rs | 24 + > pve-qemu-server-pci/src/hardware_config.rs | 156 +++++ > pve-qemu-server-pci/src/layout/legacy.rs | 628 +++++++++++++++++++ > pve-qemu-server-pci/src/layout/mod.rs | 48 ++ > pve-qemu-server-pci/src/lib.rs | 33 + > pve-qemu-server-pci/src/types/bus.rs | 198 ++++++ > pve-qemu-server-pci/src/types/device.rs | 173 +++++ > pve-qemu-server-pci/src/types/layout.rs | 329 ++++++++++ > pve-qemu-server-pci/src/types/mod.rs | 11 + > pve-qemu-server-pci/src/types/pci_address.rs | 36 ++ > rustfmt.toml | 2 + > 23 files changed, 2010 insertions(+) > create mode 100644 .cargo/config.toml > create mode 100644 .gitignore > create mode 100644 Cargo.toml > create mode 100644 Makefile > create mode 100755 build.sh > create mode 100755 bump.sh > create mode 100644 pve-qemu-server-pci/Cargo.toml > create mode 100644 pve-qemu-server-pci/debian/changelog > create mode 100644 pve-qemu-server-pci/debian/control > create mode 100644 pve-qemu-server-pci/debian/copyright > create mode 100644 pve-qemu-server-pci/debian/debcargo.toml > create mode 100644 pve-qemu-server-pci/src/constants.rs > create mode 100644 pve-qemu-server-pci/src/error.rs > create mode 100644 pve-qemu-server-pci/src/hardware_config.rs > create mode 100644 pve-qemu-server-pci/src/layout/legacy.rs > create mode 100644 pve-qemu-server-pci/src/layout/mod.rs > create mode 100644 pve-qemu-server-pci/src/lib.rs > create mode 100644 pve-qemu-server-pci/src/types/bus.rs > create mode 100644 pve-qemu-server-pci/src/types/device.rs > create mode 100644 pve-qemu-server-pci/src/types/layout.rs > create mode 100644 pve-qemu-server-pci/src/types/mod.rs > create mode 100644 pve-qemu-server-pci/src/types/pci_address.rs > create mode 100644 rustfmt.toml > > diff --git a/.cargo/config.toml b/.cargo/config.toml > new file mode 100644 > index 0000000..3b5b6e4 > --- /dev/null > +++ b/.cargo/config.toml > @@ -0,0 +1,5 @@ > +[source] > +[source.debian-packages] > +directory = "/usr/share/cargo/registry" > +[source.crates-io] > +replace-with = "debian-packages" > diff --git a/.gitignore b/.gitignore > new file mode 100644 > index 0000000..dbdfcdc > --- /dev/null > +++ b/.gitignore > @@ -0,0 +1,13 @@ > +/target > +/*/target > +Cargo.lock > +**/*.rs.bk > +*.buildinfo > +*.changes > +*.deb > +*.dsc > +*.tar.?z > +*.tar.zst > +/build > +/*-deb > +/*-dsc > diff --git a/Cargo.toml b/Cargo.toml > new file mode 100644 > index 0000000..71bc8e5 > --- /dev/null > +++ b/Cargo.toml > @@ -0,0 +1,29 @@ > +[workspace] > +members = [ > + "pve-qemu-server-pci" > +] > +exclude = [ > + "build", > +] > +resolver = "3" > + > +[workspace.package] > +authors = ["Proxmox Support Team "] > +edition = "2024" > +license = "AGPL-3" > +repository = "https://git.proxmox.com/?p=pve-qemu-server-rs.git" > +homepage = "https://proxmox.com" > +exclude = [ "debian" ] > +rust-version = "1.94" > + > +[workspace.dependencies] > +# any features enabled here are enabled on all members using 'workspace = true'! > + > +# external dependencies > +anyhow = "1.0" > +log = "0.4" > +strum = { version = "0.26", features = ["derive"] } > + > +# proxmox dependencies > +pve-api-types = "8.1" > + > diff --git a/Makefile b/Makefile > new file mode 100644 > index 0000000..f89bb73 > --- /dev/null > +++ b/Makefile > @@ -0,0 +1,153 @@ > +# Shortcut for common operations: > + > +CRATES != echo pve-*/Cargo.toml | sed -e 's|/Cargo.toml||g' > + > +# By default we just run checks: > +.PHONY: all > +all: check > + > +.PHONY: deb > +deb: $(foreach c,$(CRATES), $c-deb) > + echo $(foreach c,$(CRATES), $c-deb) > + lintian build/*.deb > + > +.PHONY: dsc > +dsc: $(foreach c,$(CRATES), $c-dsc) > + echo $(foreach c,$(CRATES), $c-dsc) > + lintian build/*.dsc > + > +.PHONY: autopkgtest > +autopkgtest: $(foreach c,$(CRATES), $c-autopkgtest) > + > +.PHONY: dinstall > +dinstall: > + $(MAKE) clean > + $(MAKE) deb > + sudo -k dpkg -i build/librust-*.deb > + > +%-deb: > + ./build.sh $* > + touch $@ > + > +proxmox-oci-deb: > + TEST_CMD="fakeroot cargo test --all-features --all-targets --release" ./build.sh proxmox-oci > + touch $@ > + > +%-dsc: > + BUILDCMD='dpkg-buildpackage -S -us -uc -d' NOTEST=1 ./build.sh $* > + touch $@ > + > +%-autopkgtest: > + autopkgtest build/$* build/*.deb -- null > + touch $@ > + > +.PHONY: list-packages > +list-packages: > + @for p in $(CRATES); do \ > + echo "librust-$$p-dev"; \ > + done > + > +.PHONY: check > +check: > + cargo test > + > +# Run the api-test server, serving the api-test/www/ subdir as 'www' dir over > +# http: > +.PHONY: apitest > +apitest: > + cargo run -p api-test -- api-test/www/ > + > +# Prints a diff between the current code and the one rustfmt would produce > +.PHONY: fmt > +fmt: > + cargo +nightly fmt -- --check > + > +# Doc without dependencies > +.PHONY: doc > +doc: > + cargo doc --no-deps > + > +.PHONY: clean > +clean: > + cargo clean > + rm -rf build/ > + rm -f -- *-deb *-dsc *-autopkgtest *.build *.buildinfo *.changes > + > +.PHONY: update > +update: > + cargo update > + > +%-upload: %-deb > + cd build; \ > + dcmd --deb rust-$*_*.changes \ > + | grep -v '.changes$$' \ > + | tar -cf "$@.tar" -T-; \ > + cat "$@.tar" | ssh -X repoman@repo.proxmox.com upload --product devel --dist trixie > + > +%-install: > + rm -rf build/install/$* > + mkdir -p build/install/$* > + BUILDDIR=build/install/$* BUILDCMD=/usr/bin/true NOCONTROL=1 ./build.sh "$*" || true > + version="$$(dpkg-parsechangelog -l $*/debian/changelog -SVersion | sed -e 's/-.*//')"; \ > + install -m755 -Dd "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \ > + rm -rf "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \ > + mv "build/install/$*/$*" \ > + "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \ > + mv "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/debian/cargo-checksum.json" \ > + "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/.cargo-checksum.json"; \ > + rm -rf "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/debian" \ > + > +.PHONY: install > +install: $(foreach c,$(CRATES), $c-install) > + > +%-install-overlay: %-install > + version="$$(dpkg-parsechangelog -l $*/debian/changelog -SVersion | sed -e 's/-.*//')"; \ > + setfattr -n trusted.overlay.opaque -v y \ > + "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}" > + install -m755 -Dd $(DESTDIR)/usr/lib/extension-release.d > + echo 'ID=_any' >$(DESTDIR)/usr/lib/extension-release.d/extension-release.$* > + > +.PHONY: install-overlay > +install-overlay: $(foreach c,$(CRATES), $c-install-overlay) > + > +# To make sure a sysext *replaces* a crate, rather than "merging" with it, we > +# need to be able to set the 'trusted.overlay.opaque' xattr. Since we cannot do > +# this as a user, we utilize `fakeroot` which keeps track of this for us, and > +# turn the final directory into an 'erofs' file system image. > +# > +# The reason is that if a crate gets changed like this: > +# > +# old: > +# src/foo.rs > +# new: > +# src/foo/mod.rs > +# > +# if its /usr/share/cargo/registry/$crate-$version directory was not marked as > +# "opaque", the merged file system would end up with both > +# > +# src/foo.rs > +# src/foo/mod.rs > +# > +# together. > +# > +# See https://docs.kernel.org/filesystems/overlayfs.html > +%-sysext: > + fakeroot $(MAKE) $*-sysext-do > +%-sysext-do: > + rm -f extensions/$*.raw > + rm -rf build/sysext/$* > + rm -rf build/install/$* > + $(MAKE) DESTDIR=build/sysext/$* $*-install-overlay > + mkdir -p extensions > + mkfs.erofs extensions/$*.raw build/sysext/$* > + > +sysext: > + fakeroot $(MAKE) sysext-do > +sysext-do: > + rm -f extensions/proxmox-workspace.raw > + [ -n "$(NOCLEAN)" ] || rm -rf build/sysext/workspace > + $(MAKE) DESTDIR=build/sysext/workspace $(foreach c,$(CRATES), $c-install) > + install -m755 -Dd build/sysext/workspace/usr/lib/extension-release.d > + echo 'ID=_any' >build/sysext/workspace/usr/lib/extension-release.d/extension-release.proxmox-workspace > + mkdir -p extensions > + mkfs.erofs extensions/proxmox-workspace.raw build/sysext/workspace > diff --git a/build.sh b/build.sh > new file mode 100755 > index 0000000..3c2293b > --- /dev/null > +++ b/build.sh > @@ -0,0 +1,40 @@ > +#!/bin/sh > + > +set -e > + > +export CARGO=/usr/bin/cargo > +export RUSTC=/usr/bin/rustc > + > +CRATE=$1 > +BUILDCMD=${BUILDCMD:-"dpkg-buildpackage -b -uc -us"} > +BUILDDIR="${BUILDDIR:-"build"}" > +TEST_CMD="${TEST_CMD:-"$CARGO test --all-features --all-targets --release"}" > + > +mkdir -p "${BUILDDIR}" > +echo system >"${BUILDDIR}"/rust-toolchain > +rm -rf "${BUILDDIR}/${CRATE}" > + > +CONTROL="$PWD/${CRATE}/debian/control" > + > +if [ -e "$CONTROL" ]; then > + # check but only warn, debcargo fails anyway if crates are missing > + dpkg-checkbuilddeps $PWD/${CRATE}/debian/control || true > + [ "x$NOCONTROL" = 'x' ] && rm -f "$PWD/${CRATE}/debian/control" > +fi > + > +debcargo package \ > + --config "$PWD/${CRATE}/debian/debcargo.toml" \ > + --changelog-ready \ > + --no-overlay-write-back \ > + --directory "$PWD/"${BUILDDIR}"/${CRATE}" \ > + "${CRATE}" \ > + "$(dpkg-parsechangelog -l "${CRATE}/debian/changelog" -SVersion | sed -e 's/-.*//')" > + > +cd "${BUILDDIR}/${CRATE}" > +rm -f debian/source/format.debcargo.hint > +${BUILDCMD} > + > +# needs all crates build-dependencies, which can be more than what debcargo assembles. > +[ "x$NOTEST" = "x" ] && $TEST_CMD > + > +[ "x$NOCONTROL" = "x" ] && cp debian/control "$CONTROL" > diff --git a/bump.sh b/bump.sh > new file mode 100755 > index 0000000..08ad119 > --- /dev/null > +++ b/bump.sh > @@ -0,0 +1,44 @@ > +#!/bin/bash > + > +package=$1 > + > +if [[ -z "$package" ]]; then > + echo "USAGE:" > + echo -e "\t bump.sh [patch|minor|major|]" > + echo "" > + echo "Defaults to bumping patch version by 1" > + exit 0 > +fi > + > +cargo_set_version="$(command -v cargo-set-version)" > +if [[ -z "$cargo_set_version" || ! -x "$cargo_set_version" ]]; then > + echo 'bump.sh requires "cargo set-version", provided by "cargo-edit".' > + exit 1 > +fi > + > +if [[ ! -e "$package/Cargo.toml" ]]; then > + echo "Invalid crate '$package'" > + exit 1 > +fi > + > +version=$2 > +if [[ -z "$version" ]]; then > + version="patch" > +fi > + > +case "$version" in > + patch|minor|major) > + bump="--bump" > + ;; > + *) > + bump= > + ;; > +esac > + > +cargo_toml="$package/Cargo.toml" > +changelog="$package/debian/changelog" > + > +cargo set-version -p "$package" $bump "$version" > +version="$(cargo metadata --format-version=1 | jq ".packages[] | select(.name == \"$package\").version" | sed -e 's/\"//g')" > +DEBFULLNAME="Proxmox Support Team" DEBEMAIL="support@proxmox.com" dch --no-conf --changelog "$changelog" --newversion "$version-1" --distribution stable > +git commit --edit -sm "bump $package to $version-1" Cargo.toml "$cargo_toml" "$changelog" > diff --git a/pve-qemu-server-pci/Cargo.toml b/pve-qemu-server-pci/Cargo.toml > new file mode 100644 > index 0000000..d3f4401 > --- /dev/null > +++ b/pve-qemu-server-pci/Cargo.toml > @@ -0,0 +1,15 @@ > +[package] > +name = "pve-qemu-server-pci" > +version = "0.1.0" > +authors.workspace = true > +edition.workspace = true > +license.workspace = true > +repository.workspace = true > +homepage.workspace = true > +exclude.workspace = true > +rust-version.workspace = true > + > +[dependencies] > +strum.workspace = true > + > +pve-api-types.workspace = true > diff --git a/pve-qemu-server-pci/debian/changelog b/pve-qemu-server-pci/debian/changelog > new file mode 100644 > index 0000000..54847b9 > --- /dev/null > +++ b/pve-qemu-server-pci/debian/changelog > @@ -0,0 +1,5 @@ > +rust-pve-qemu-server-pci (0.1.0) trixie; urgency=medium > + > + * initial release > + > + -- Proxmox Support Team Tue, 01 Sep 2026 14:02:25 +0200 > diff --git a/pve-qemu-server-pci/debian/control b/pve-qemu-server-pci/debian/control > new file mode 100644 > index 0000000..9edd0b9 > --- /dev/null > +++ b/pve-qemu-server-pci/debian/control > @@ -0,0 +1,36 @@ > +Source: rust-pve-qemu-server-pci > +Section: rust > +Priority: optional > +Build-Depends: debhelper-compat (= 13), > + dh-sequence-cargo > +Build-Depends-Arch: cargo:native , > + rustc:native (>= 1.94) , > + libstd-rust-dev , > + librust-pve-api-types-8+default-dev (>= 8.1-~~) , > + librust-strum-0.26+default-dev , > + librust-strum-0.26+derive-dev > +Maintainer: Proxmox Support Team > +Standards-Version: 4.7.2 > +Vcs-Git: git://git.proxmox.com/git/pve-qemu-server-rs.git > +Vcs-Browser: https://git.proxmox.com/?p=pve-qemu-server-rs.git > +Homepage: https://proxmox.com > +X-Cargo-Crate: pve-qemu-server-pci > + > +Package: librust-pve-qemu-server-pci-dev > +Architecture: any > +Multi-Arch: same > +Depends: > + ${misc:Depends}, > + librust-pve-api-types-8+default-dev (>= 8.1-~~), > + librust-strum-0.26+default-dev, > + librust-strum-0.26+derive-dev > +Provides: > + librust-pve-qemu-server-pci+default-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0+default-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0.1-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0.1+default-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0.1.0-dev (= ${binary:Version}), > + librust-pve-qemu-server-pci-0.1.0+default-dev (= ${binary:Version}) > +Description: Rust crate "pve-qemu-server-pci" - Rust source code > + Source code for Debianized Rust crate "pve-qemu-server-pci" > diff --git a/pve-qemu-server-pci/debian/copyright b/pve-qemu-server-pci/debian/copyright > new file mode 100644 > index 0000000..01138fa > --- /dev/null > +++ b/pve-qemu-server-pci/debian/copyright > @@ -0,0 +1,18 @@ > +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ > + > +Files: > + * > +Copyright: 2026 Proxmox Server Solutions GmbH > +License: AGPL-3.0-or-later > + This program is free software: you can redistribute it and/or modify it under > + the terms of the GNU Affero General Public License as published by the Free > + Software Foundation, either version 3 of the License, or (at your option) any > + later version. > + . > + This program is distributed in the hope that it will be useful, but WITHOUT > + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS > + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more > + details. > + . > + You should have received a copy of the GNU Affero General Public License along > + with this program. If not, see . > diff --git a/pve-qemu-server-pci/debian/debcargo.toml b/pve-qemu-server-pci/debian/debcargo.toml > new file mode 100644 > index 0000000..1d52b16 > --- /dev/null > +++ b/pve-qemu-server-pci/debian/debcargo.toml > @@ -0,0 +1,7 @@ > +overlay = "." > +crate_src_path = ".." > +maintainer = "Proxmox Support Team " > + > +[source] > +vcs_git = "git://git.proxmox.com/git/pve-qemu-server-rs.git" > +vcs_browser = "https://git.proxmox.com/?p=pve-qemu-server-rs.git" > diff --git a/pve-qemu-server-pci/src/constants.rs b/pve-qemu-server-pci/src/constants.rs > new file mode 100644 > index 0000000..ea6db21 > --- /dev/null > +++ b/pve-qemu-server-pci/src/constants.rs > @@ -0,0 +1,7 @@ > +//! Limits the layouts are built around. > + > +/// Number of slots on a PCI bus. Slot 0x00 is always taken by the bridge > +/// itself, so only [`USABLE_SLOTS_PER_BUS`] of them can hold a device. > +pub(crate) const SLOTS_PER_BUS: usize = 32; > +pub(crate) const USABLE_SLOTS_PER_BUS: u8 = SLOTS_PER_BUS as u8 - 1; > +pub(crate) const FUNCTIONS_PER_DEVICE: usize = 8; > diff --git a/pve-qemu-server-pci/src/error.rs b/pve-qemu-server-pci/src/error.rs > new file mode 100644 > index 0000000..5c4f7de > --- /dev/null > +++ b/pve-qemu-server-pci/src/error.rs > @@ -0,0 +1,24 @@ > +use std::{error::Error, fmt::Display}; > + > +use crate::Device; > + > +/// Errors that can occur while looking up a PCI address. > +#[derive(Clone, Copy, Debug, PartialEq, Eq)] > +#[non_exhaustive] > +pub enum LayoutError { > + /// The given configuration id is not a device this crate knows about. > + InvalidConfigId, > + /// The device is not part of the layout it was looked up in. > + NoAddressFound(Device), > +} > + > +impl Display for LayoutError { > + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + match self { > + LayoutError::InvalidConfigId => write!(f, "invalid configuration id"), > + LayoutError::NoAddressFound(device) => write!(f, "address '{device}' not found"), > + } > + } > +} > + > +impl Error for LayoutError {} > diff --git a/pve-qemu-server-pci/src/hardware_config.rs b/pve-qemu-server-pci/src/hardware_config.rs > new file mode 100644 > index 0000000..a348353 > --- /dev/null > +++ b/pve-qemu-server-pci/src/hardware_config.rs > @@ -0,0 +1,156 @@ > +use std::sync::Arc; > +use std::sync::atomic::AtomicU16; > + > +use pve_api_types::{ClusterResourceHostArch as Arch, QemuConfigOstype, QemuConfigScsihw}; > + > +#[derive(Default, Clone, Debug, PartialEq)] > +pub enum Mainboard { > + #[default] > + I440fx, > + Q35, > + Virt, > +} > + > +impl std::fmt::Display for Mainboard { > + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + let text = match self { > + Mainboard::I440fx => "i440fx", > + Mainboard::Q35 => "q35", > + Mainboard::Virt => "virt", > + }; > + f.write_str(text) > + } > +} > + > +impl Mainboard { > + pub fn has_pcie(&self) -> bool { > + match self { > + Mainboard::I440fx => false, > + Mainboard::Q35 | Mainboard::Virt => true, > + } > + } > +} > + > +/// The parts of a guest configuration that influence which PCI layout is used > +/// and which quirks have to be applied to it. > +/// > +/// This is deliberately not the full guest config: it only carries what the > +/// address generation needs, so that callers can build it from whatever > +/// configuration representation they have. > +#[derive(Default, Clone, Debug)] > +pub struct HardwareConfig { > + pub arch: Arch, > + pub mainboard: Mainboard, > + pub scsihw: Option, > + /// Highest `scsihw` controller index in use. Controllers from index 2 on > + /// live on `pci.4`, so this decides whether that bridge is needed. > + pub max_scsihw: u8, > + pub legacy_igd: bool, > + pub ostype: Option, > + /// QEMU machine version as `(major, minor, pve)`, where the PVE specific > + /// revision is optional. > + pub machine_version: (u16, u16, Option), > + /// The recorded required `+pveX` version > + pub required_pve_version: Arc, > +} > + > +impl HardwareConfig { > + /// Builds a [`HardwareConfig`] from the raw configuration values. > + /// > + /// Unparsable `arch`, `scsihw` and `ostype` values are not an error here: > + /// `arch` falls back to x86_64 and the other two to `None`, which is the > + /// same defaulting the Perl implementation does. But does it make sense to act like this *here*? I'm thinking it may make more sense for that to happen at the `pve-rs` side, to not let this creep in any further in the future? > + #[allow(clippy::too_many_arguments)] > + pub fn new( > + arch: &str, > + machine_type: String, > + scsihw: Option, > + max_scsihw: u8, > + legacy_igd: bool, > + ostype: Option, > + major: u16, > + minor: u16, > + pve: Option, > + ) -> Self { > + let arch = match arch.parse().unwrap_or(Arch::X8664) { > + Arch::UnknownEnumValue(_) => Arch::X8664, > + other => other, > + }; > + let scsihw = scsihw.and_then(|hw| hw.parse().ok()); > + let ostype = ostype.and_then(|ostype| ostype.parse().ok()); > + > + let mainboard = match arch { > + Arch::Aarch64 => Mainboard::Virt, > + Arch::X8664 if machine_type.contains("q35") => Mainboard::Q35, > + Arch::X8664 | Arch::UnknownEnumValue(_) => Mainboard::I440fx, > + }; > + Self { > + arch, > + mainboard, > + scsihw, > + max_scsihw, > + legacy_igd, > + ostype, > + machine_version: (major, minor, pve), > + required_pve_version: Arc::new(AtomicU16::new(0)), > + } > + } > + > + /// Whether the guest's root bus is PCIe. True for q35 machines and for > + /// aarch64, which only has a PCIe host bridge. > + pub fn has_pcie(&self) -> bool { > + self.mainboard.has_pcie() > + } > + > + /// Returns if the mainboard is a Q35 variant. > + pub fn is_q35(&self) -> bool { > + self.mainboard == Mainboard::Q35 > + } > + > + /// Whether each SCSI disk gets its own controller, which needs the extra > + /// `pci.3` bridge. > + pub fn virtio_scsi_single(&self) -> bool { > + self.scsihw == Some(QemuConfigScsihw::VirtioScsiSingle) > + } > + > + /// Whether the machine version is at least `major.minor.pve`. > + /// > + /// A machine without a PVE revision counts as '+pve0'. > + pub fn version_at_least(&self, major: u16, minor: u16, pve: Option) -> bool { > + match (self.machine_version.0, self.machine_version.1).cmp(&(major, minor)) { > + std::cmp::Ordering::Less => false, > + std::cmp::Ordering::Greater => true, > + std::cmp::Ordering::Equal => { > + let ours = self.machine_version.2.unwrap_or(0); > + let pve = pve.unwrap_or(0); > + let ok = ours >= pve; > + if ok && pve > 0 { > + self.required_pve_version > + .fetch_max(pve, std::sync::atomic::Ordering::SeqCst); This can also be `Ordering::Relaxed`. > + } > + ok > + } > + } > + } > + > + /// Returns the version number for Windows OS types or 0 for other. > + /// > + /// Windows XP era OS types are represented by `5`, and Vista era ones by `6`. > + pub fn windows_version(&self) -> u8 { > + match self.ostype.unwrap_or(QemuConfigOstype::Other) { > + QemuConfigOstype::Wxp | QemuConfigOstype::W2k | QemuConfigOstype::W2k3 => 5, > + QemuConfigOstype::W2k8 | QemuConfigOstype::Wvista => 6, > + QemuConfigOstype::Win7 => 7, > + QemuConfigOstype::Win8 => 8, > + QemuConfigOstype::Win10 => 10, > + QemuConfigOstype::Win11 => 11, > + _ => 0, > + } > + } > + > + /// Returns the recorded required `+pveX` version. > + pub fn required_pve_version(&self) -> u16 { > + self.required_pve_version > + .load(std::sync::atomic::Ordering::Relaxed) > + } > +} > diff --git a/pve-qemu-server-pci/src/layout/legacy.rs b/pve-qemu-server-pci/src/layout/legacy.rs > new file mode 100644 > index 0000000..e6693ca > --- /dev/null > +++ b/pve-qemu-server-pci/src/layout/legacy.rs > @@ -0,0 +1,628 @@ > +use crate::constants::SLOTS_PER_BUS; > +use crate::hardware_config::Mainboard; > +use crate::layout::bridge_slots; > +use crate::{Bus, Device, Function, HardwareConfig, Layout, LayoutError, PciAddress, Slot}; > + > +const PCI_1: [Slot; SLOTS_PER_BUS] = bridge_slots(&[ > + single_device!(Net(6)), > + single_device!(Net(7)), > + single_device!(Net(8)), > + single_device!(Net(9)), > + single_device!(Net(10)), > + single_device!(Net(11)), > + single_device!(Net(12)), > + single_device!(Net(13)), > + single_device!(Net(14)), > + single_device!(Net(15)), > + single_device!(Net(16)), > + single_device!(Net(17)), > + single_device!(Net(18)), > + single_device!(Net(19)), > + single_device!(Net(20)), > + single_device!(Net(21)), > + single_device!(Net(22)), > + single_device!(Net(23)), > + single_device!(Net(24)), > + single_device!(Net(25)), > + single_device!(Net(26)), > + single_device!(Net(27)), > + single_device!(Net(28)), > + single_device!(Net(29)), > + single_device!(Net(30)), > + single_device!(Net(31)), > + single_device!(XhciController(0)), > + Slot::Single(Function::FixedBridge(Bus::Pci(4), &PCI_4)), > + single_device!(Rng), > + single_device!(Bridge(Bus::Pci(99))), // same as pci.2 but for igd > +]); > + > +const PCI_2: [Slot; SLOTS_PER_BUS] = bridge_slots(&[ > + single_device!(VirtioBlk(6)), > + single_device!(VirtioBlk(7)), > + single_device!(VirtioBlk(8)), > + single_device!(VirtioBlk(9)), > + single_device!(VirtioBlk(10)), > + single_device!(VirtioBlk(11)), > + single_device!(VirtioBlk(12)), > + single_device!(VirtioBlk(13)), > + single_device!(VirtioBlk(14)), > + single_device!(VirtioBlk(15)), > + single_device!(Ivshmem), > + single_device!(Audio), > + single_device!(Hostpci(4)), > + single_device!(Hostpci(5)), > + single_device!(Hostpci(6)), > + single_device!(Hostpci(7)), > + single_device!(Hostpci(8)), > + single_device!(Hostpci(9)), > + single_device!(Hostpci(10)), > + single_device!(Hostpci(11)), > + single_device!(Hostpci(12)), > + single_device!(Hostpci(13)), > + single_device!(Hostpci(14)), > + single_device!(Hostpci(15)), > +]); > + > +const PCI_3: [Slot; SLOTS_PER_BUS] = bridge_slots(&[ > + single_device!(ScsiController(0)), > + single_device!(ScsiController(1)), > + single_device!(ScsiController(2)), > + single_device!(ScsiController(3)), > + single_device!(ScsiController(4)), > + single_device!(ScsiController(5)), > + single_device!(ScsiController(6)), > + single_device!(ScsiController(7)), > + single_device!(ScsiController(8)), > + single_device!(ScsiController(9)), > + single_device!(ScsiController(10)), > + single_device!(ScsiController(11)), > + single_device!(ScsiController(12)), > + single_device!(ScsiController(13)), > + single_device!(ScsiController(14)), > + single_device!(ScsiController(15)), > + single_device!(ScsiController(16)), > + single_device!(ScsiController(17)), > + single_device!(ScsiController(18)), > + single_device!(ScsiController(19)), > + single_device!(ScsiController(20)), > + single_device!(ScsiController(21)), > + single_device!(ScsiController(22)), > + single_device!(ScsiController(23)), > + single_device!(ScsiController(24)), > + single_device!(ScsiController(25)), > + single_device!(ScsiController(26)), > + single_device!(ScsiController(27)), > + single_device!(ScsiController(28)), > + single_device!(ScsiController(29)), > + single_device!(ScsiController(30)), > +]); > + > +const PCI_4: [Slot; SLOTS_PER_BUS] = bridge_slots(&[ > + single_device!(ScsiController(2)), > + single_device!(ScsiController(3)), > + single_device!(ScsiController(4)), > +]); > + > +/// Builds the PCI layout of a guest. > +/// > +/// With `virtio_scsi_single` the SCSI controllers move off the root bus onto > +/// their own `pci.3` bridge, which is why this cannot be a constant. > +pub(crate) fn get_legacy_pci_layout<'a>(pcie: bool, virtio_scsi_single: bool) -> Layout<'a> { A rogue lifetime appears. Drop it and return `Layout<'static>`. > + Layout { > + pcie, > + root: [ > + Slot::Reserved, > + single_device!(Piix3Controller), // actually on 0x01.2 but the function number is added by QemuServer/USB.pm > + single_device!(Vga(0)), > + single_device!(Balloon), > + single_device!(Watchdog), > + if virtio_scsi_single { > + Slot::Single(Function::FixedBridge(Bus::Pci(3), &PCI_3)) > + } else { > + single_device!(ScsiController(0)) > + }, > + single_device!(ScsiController(1)), > + single_device!(Ahci), > + single_device!(GuestAgent), > + single_device!(SpiceSerial), > + single_device!(VirtioBlk(0)), > + single_device!(VirtioBlk(1)), > + single_device!(VirtioBlk(2)), > + single_device!(VirtioBlk(3)), > + single_device!(VirtioBlk(4)), > + single_device!(VirtioBlk(5)), > + single_device!(Hostpci(0)), > + single_device!(Hostpci(1)), > + single_device!(Net(0)), > + single_device!(Net(1)), > + single_device!(Net(2)), > + single_device!(Net(3)), > + single_device!(Net(4)), > + single_device!(Net(5)), > + single_device!(Vga(1)), > + single_device!(Vga(2)), > + single_device!(Vga(3)), > + single_device!(Hostpci(2)), > + single_device!(Hostpci(3)), > + Slot::Reserved, // usb-host (pve-usb.cfg) > + Slot::Single(Function::FixedBridge(Bus::Pci(1), &PCI_1)), > + Slot::Single(Function::FixedBridge(Bus::Pci(2), &PCI_2)), > + ], > + } > +} > + > +/// The PCIe layout of a q35 guest. > +/// > +/// Only contains the devices on the PCIe bus itself, not the ones nested on > +/// the PCI bridges below it. > +#[expect(deprecated)] > +pub(crate) const LEGACY_PCIE_LAYOUT: Layout = Layout { > + pcie: true, > + root: [ > + Slot::Reserved, > + single_device!(Vga(0)), > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Multi(&[ > + Function::RootPort(Bus::LegacyRootPort(5), Device::Hostpcie(4)), > + Function::RootPort(Bus::LegacyRootPort(6), Device::Hostpcie(5)), > + Function::RootPort(Bus::LegacyRootPort(7), Device::Hostpcie(6)), > + Function::RootPort(Bus::LegacyRootPort(8), Device::Hostpcie(7)), > + Function::RootPort(Bus::LegacyRootPort(9), Device::Hostpcie(8)), > + Function::RootPort(Bus::LegacyRootPort(10), Device::Hostpcie(9)), > + Function::RootPort(Bus::LegacyRootPort(11), Device::Hostpcie(10)), > + Function::RootPort(Bus::LegacyRootPort(12), Device::Hostpcie(11)), > + ]), > + Slot::Multi(&[ > + Function::RootPort(Bus::LegacyRootPort(13), Device::Hostpcie(12)), > + Function::RootPort(Bus::LegacyRootPort(14), Device::Hostpcie(13)), > + Function::RootPort(Bus::LegacyRootPort(15), Device::Hostpcie(14)), > + Function::RootPort(Bus::LegacyRootPort(16), Device::Hostpcie(15)), > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Unused, > + ]), > + Slot::Unused, > + Slot::Unused, > + single_device!(Ivshmem), > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Multi(&[ > + Function::Device(Device::Ich9Uhci(4)), > + Function::Device(Device::Ich9Uhci(5)), > + Function::Device(Device::Ich9Uhci(6)), > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Device(Device::Ich9Ehci(2)), > + ]), > + single_device!(Ich9Audio), > + Slot::Multi(&[ > + Function::RootPort(Bus::LegacyRootPort(1), Device::Hostpcie(0)), > + Function::RootPort(Bus::LegacyRootPort(2), Device::Hostpcie(1)), > + Function::RootPort(Bus::LegacyRootPort(3), Device::Hostpcie(2)), > + Function::RootPort(Bus::LegacyRootPort(4), Device::Hostpcie(3)), > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Unused, > + ]), > + Slot::Multi(&[ > + Function::Device(Device::Ich9Uhci(1)), > + Function::Device(Device::Ich9Uhci(2)), > + Function::Device(Device::Ich9Uhci(3)), > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Device(Device::Ich9Ehci(1)), > + ]), > + Slot::Single(Function::FixedBridge( > + Bus::Pcidmi, > + &bridge_slots(&[ > + single_device!(Bridge(Bus::Pci(0))), > + single_device!(Bridge(Bus::Pci(1))), > + single_device!(Bridge(Bus::Pci(2))), > + single_device!(Bridge(Bus::Pci(3))), > + ]), > + )), > + Slot::Reserved, // ICH9 LPC, SATA and SMBus controllers > + ], > +}; > + > +/// The PCIe layout for Windows 7 guests, whose driver cannot cope with > +/// passed through devices behind root ports. > +/// > +/// Only contains the `hostpci` slots, everything else is handled by > +/// [`LEGACY_PCIE_LAYOUT`]. > +pub(crate) const LEGACY_PCIE_LAYOUT_WIN7: Layout = Layout { > + pcie: true, > + root: [ > + Slot::Reserved, > + Slot::Reserved, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + single_device!(Hostpcie(4)), > + single_device!(Hostpcie(5)), > + single_device!(Hostpcie(6)), > + single_device!(Hostpcie(7)), > + single_device!(Hostpcie(8)), > + single_device!(Hostpcie(9)), > + single_device!(Hostpcie(10)), > + single_device!(Hostpcie(0)), > + single_device!(Hostpcie(1)), > + single_device!(Hostpcie(2)), > + single_device!(Hostpcie(3)), > + Slot::Reserved, > + single_device!(Hostpcie(11)), > + single_device!(Hostpcie(12)), > + single_device!(Hostpcie(13)), > + single_device!(Hostpcie(14)), > + single_device!(Hostpcie(15)), > + Slot::Unused, > + Slot::Unused, > + Slot::Reserved, > + Slot::Unused, > + Slot::Unused, > + Slot::Unused, > + ], > +}; > + > +/// Whether a (mapped) id asks for the `virtio-scsi-single` variant of the > +/// layout, where every SCSI disk gets its own controller on `pci.3`. > +fn is_virtio_scsi_single_id(id: &str) -> bool { > + id.starts_with("virtioscsi") > +} > + > +/// Maps (conflicting) legacy PCI IDs to ones we can represent here. > +fn map_legacy_pci_id(original: &str) -> &str { > + // we can't request scsihwX and virtioscsiX at the same time, > + // and that's the only thing living on 'pci.3' > + if original == "pci.3" { > + return "scsihw0"; > + } > + > + // the legacy-igd device must be the same as the primary adapter > + // and requires vga=none > + if original == "legacy-igd" { > + return "vga"; > + } > + > + // when using legacy-igd, pci.2 is on a different address due to > + // driver constraints > + if original == "pci.2-igd" { > + return "pci.99"; > + } nit: might be nicer to read as: match original { "a" => "b", "x" => "y", o => o, } > + > + original > +} > + > +/// Looks up the PCI bridge a device sits on, see > +/// [`crate::pci_bridge_for_device`]. > +pub(crate) fn pci_bridge_for_device(id: &str) -> Option { > + let new_id = map_legacy_pci_id(id); > + // the root bus is never a bridge we have to add ourselves, so it does not > + // matter whether it is PCI or PCIe here > + let layout = get_legacy_pci_layout(false, is_virtio_scsi_single_id(new_id)); > + let device = new_id.parse().ok()?; > + let addr = layout.find_device(&device).ok()?; > + Some(addr.bus) > +} > + > +/// Returns if the caller wants a PCIE address instead of a PCI one based on the id. > +fn is_pcie_id(id: &str) -> bool { > + id.starts_with("hostpcie") || id == "vga" || id == "ivshmem" > +} > + > +pub(crate) fn address(hw: &HardwareConfig, id: &str) -> Result { > + let (layout, new_id) = match hw.mainboard { > + Mainboard::Q35 if is_pcie_id(id) => { > + let layout = if hw.windows_version() == 7 && id.starts_with("hostpcie") { > + LEGACY_PCIE_LAYOUT_WIN7 > + } else { > + LEGACY_PCIE_LAYOUT > + }; > + (layout, id) > + } > + Mainboard::Q35 | Mainboard::Virt | Mainboard::I440fx => { > + let new_id = map_legacy_pci_id(id); > + let layout = get_legacy_pci_layout( > + hw.mainboard == Mainboard::Virt, > + is_virtio_scsi_single_id(id), > + ); > + (layout, new_id) > + } > + }; > + > + layout.find_device(&new_id.parse()?) > +} > + > +#[cfg(test)] > +pub(crate) mod test { > + use std::collections::HashSet; > + > + use super::{LEGACY_PCIE_LAYOUT, get_legacy_pci_layout, pci_bridge_for_device}; > + use crate::hardware_config::Mainboard; > + use crate::layout::legacy::is_pcie_id; > + use crate::{Bus, Device, HardwareConfig, Layout, PciAddress}; > + > + pub const LEGACY_ADDRS: [(&str, u8, u8); 121] = [ > + ("ahci0", 0, 7), > + ("audio0", 2, 12), > + ("balloon0", 0, 3), > + ("ehci", 0, 1), > + ("hostpci0", 0, 16), > + ("hostpci1", 0, 17), > + ("hostpci2", 0, 27), > + ("hostpci3", 0, 28), > + ("hostpci4", 2, 13), > + ("hostpci5", 2, 14), > + ("hostpci6", 2, 15), > + ("hostpci7", 2, 16), > + ("hostpci8", 2, 17), > + ("hostpci9", 2, 18), > + ("hostpci10", 2, 19), > + ("hostpci11", 2, 20), > + ("hostpci12", 2, 21), > + ("hostpci13", 2, 22), > + ("hostpci14", 2, 23), > + ("hostpci15", 2, 24), > + ("ivshmem", 2, 11), > + ("legacy-igd", 0, 2), > + ("net0", 0, 18), > + ("net1", 0, 19), > + ("net2", 0, 20), > + ("net3", 0, 21), > + ("net4", 0, 22), > + ("net5", 0, 23), > + ("net6", 1, 1), > + ("net7", 1, 2), > + ("net8", 1, 3), > + ("net9", 1, 4), > + ("net10", 1, 5), > + ("net11", 1, 6), > + ("net12", 1, 7), > + ("net13", 1, 8), > + ("net14", 1, 9), > + ("net15", 1, 10), > + ("net16", 1, 11), > + ("net17", 1, 12), > + ("net18", 1, 13), > + ("net19", 1, 14), > + ("net20", 1, 15), > + ("net21", 1, 16), > + ("net22", 1, 17), > + ("net23", 1, 18), > + ("net24", 1, 19), > + ("net25", 1, 20), > + ("net26", 1, 21), > + ("net27", 1, 22), > + ("net28", 1, 23), > + ("net29", 1, 24), > + ("net30", 1, 25), > + ("net31", 1, 26), > + ("pci.1", 0, 30), > + ("pci.2", 0, 31), > + ("pci.2-igd", 1, 30), > + ("pci.3", 0, 5), > + ("pci.4", 1, 28), > + ("piix3", 0, 1), > + ("qga0", 0, 8), > + ("rng0", 1, 29), > + ("scsihw0", 0, 5), > + ("scsihw1", 0, 6), > + ("scsihw2", 4, 1), > + ("scsihw3", 4, 2), > + ("scsihw4", 4, 3), > + ("spice", 0, 9), > + ("vga", 0, 2), > + ("vga1", 0, 24), > + ("vga2", 0, 25), > + ("vga3", 0, 26), > + ("virtio0", 0, 10), > + ("virtio1", 0, 11), > + ("virtio2", 0, 12), > + ("virtio3", 0, 13), > + ("virtio4", 0, 14), > + ("virtio5", 0, 15), > + ("virtio6", 2, 1), > + ("virtio7", 2, 2), > + ("virtio8", 2, 3), > + ("virtio9", 2, 4), > + ("virtio10", 2, 5), > + ("virtio11", 2, 6), > + ("virtio12", 2, 7), > + ("virtio13", 2, 8), > + ("virtio14", 2, 9), > + ("virtio15", 2, 10), > + ("virtioscsi0", 3, 1), > + ("virtioscsi1", 3, 2), > + ("virtioscsi2", 3, 3), > + ("virtioscsi3", 3, 4), > + ("virtioscsi4", 3, 5), > + ("virtioscsi5", 3, 6), > + ("virtioscsi6", 3, 7), > + ("virtioscsi7", 3, 8), > + ("virtioscsi8", 3, 9), > + ("virtioscsi9", 3, 10), > + ("virtioscsi10", 3, 11), > + ("virtioscsi11", 3, 12), > + ("virtioscsi12", 3, 13), > + ("virtioscsi13", 3, 14), > + ("virtioscsi14", 3, 15), > + ("virtioscsi15", 3, 16), > + ("virtioscsi16", 3, 17), > + ("virtioscsi17", 3, 18), > + ("virtioscsi18", 3, 19), > + ("virtioscsi19", 3, 20), > + ("virtioscsi20", 3, 21), > + ("virtioscsi21", 3, 22), > + ("virtioscsi22", 3, 23), > + ("virtioscsi23", 3, 24), > + ("virtioscsi24", 3, 25), > + ("virtioscsi25", 3, 26), > + ("virtioscsi26", 3, 27), > + ("virtioscsi27", 3, 28), > + ("virtioscsi28", 3, 29), > + ("virtioscsi29", 3, 30), > + ("virtioscsi30", 3, 31), > + ("watchdog", 0, 4), > + ("xhci", 1, 27), > + ]; > + > + fn pci_addr(pcie: bool, bus: u8, device: u8) -> PciAddress { > + PciAddress { > + bus: if pcie { Bus::Pcie(bus) } else { Bus::Pci(bus) }, > + device, > + function: 0, > + } > + } > + > + #[test] > + /// Test string id to addr, big overlap with config2command tests in qemu-server > + fn test_pci_addr_maps() { > + // copied and sorted from src/PVE/QemuServer/PCI.pm > + > + let i440fx = HardwareConfig::default(); > + let q35 = HardwareConfig { > + mainboard: Mainboard::Q35, > + ..Default::default() > + }; > + let arm = HardwareConfig { > + arch: pve_api_types::ClusterResourceHostArch::Aarch64, > + mainboard: Mainboard::Virt, > + ..Default::default() > + }; > + for (input, bus, device) in LEGACY_ADDRS { > + let addr = > + crate::address(&i440fx, input).unwrap_or_else(|_| panic!("{input} not found")); > + let expected = pci_addr(false, bus, device); > + assert_eq!(addr, expected, "tested {input} on i440fx"); > + > + if !is_pcie_id(input) { > + let addr = > + crate::address(&q35, input).unwrap_or_else(|_| panic!("{input} not found")); > + let expected = pci_addr(false, bus, device); > + assert_eq!(addr, expected, "tested {input} on q35"); > + } > + > + // arm is on pcie > + let addr = crate::address(&arm, input).unwrap_or_else(|_| panic!("{input} not found")); > + let expected = pci_addr(bus == 0, bus, device); > + assert_eq!(addr, expected, "tested {input} on virt"); > + } > + } > + > + #[test] > + /// Test string id to addr, big overlap with config2command tests in qemu-server > + fn test_pcie_addr_maps() { > + // copied and sorted from src/PVE/QemuServer/PCI.pm > + let tests = [ > + ("hostpcie0", true, 1), > + ("hostpcie1", true, 2), > + ("hostpcie2", true, 3), > + ("hostpcie3", true, 4), > + ("hostpcie4", true, 5), > + ("hostpcie5", true, 6), > + ("hostpcie6", true, 7), > + ("hostpcie7", true, 8), > + ("hostpcie8", true, 9), > + ("hostpcie9", true, 10), > + ("hostpcie10", true, 11), > + ("hostpcie11", true, 12), > + ("hostpcie12", true, 13), > + ("hostpcie13", true, 14), > + ("hostpcie14", true, 15), > + ("hostpcie15", true, 16), > + ("ivshmem", false, 20), > + ("vga", false, 1), > + ]; > + > + let pcie_machine = HardwareConfig { > + mainboard: Mainboard::Q35, > + ..Default::default() > + }; > + for (input, rp, num) in tests { > + let addr = crate::address(&pcie_machine, input) > + .unwrap_or_else(|_| panic!("device not found: {input}")); > + let (bus, device) = if rp { > + #[expect(deprecated)] > + (Bus::LegacyRootPort(num), 0) > + } else { > + (Bus::Pcie(0), num) > + }; > + let expected = PciAddress::new(bus, device, 0); > + assert_eq!(addr, expected, "tested {input}"); > + } > + } > + > + /// A bus can only be used once the bridge or root port creating it has > + /// been yielded, otherwise a command line built in iteration order would > + /// reference a bus QEMU does not know yet. > + fn assert_buses_before_devices(layout: &Layout) { > + let mut buses = HashSet::from([if layout.pcie { > + Bus::Pcie(0) > + } else { > + Bus::Pci(0) > + }]); > + > + for used in layout.iter() { > + assert!( > + buses.contains(&used.addr.bus), > + "{:?} is on {:?}, which was not created yet", > + used.device, > + used.addr.bus > + ); > + match used.device { > + Device::Bridge(bus) => buses.insert(bus), > + Device::RootPort(bus) => buses.insert(bus), > + _ => continue, > + }; > + } > + } > + > + #[test] > + fn test_bus_order() { > + assert_buses_before_devices(&LEGACY_PCIE_LAYOUT); > + assert_buses_before_devices(&get_legacy_pci_layout(false, false)); > + assert_buses_before_devices(&get_legacy_pci_layout(false, true)); > + assert_buses_before_devices(&get_legacy_pci_layout(true, true)); > + } > + > + #[test] > + /// The bus of a device has to agree with the address it is given. Note > + /// that the root bus is reported as bus zero, not as no bus at all. > + fn test_pci_bridge_for_device() { > + for (input, bus, _) in LEGACY_ADDRS { > + assert_eq!( > + pci_bridge_for_device(input), > + Some(Bus::Pci(bus)), > + "for {input}" > + ); > + } > + > + assert_eq!(pci_bridge_for_device("no-such-device"), None); > + } > +} > diff --git a/pve-qemu-server-pci/src/layout/mod.rs b/pve-qemu-server-pci/src/layout/mod.rs > new file mode 100644 > index 0000000..5439c17 > --- /dev/null > +++ b/pve-qemu-server-pci/src/layout/mod.rs > @@ -0,0 +1,48 @@ > +use crate::Slot; > +use crate::constants::SLOTS_PER_BUS; > + > +// local helper macro to make the lines shorter > +macro_rules! single_device { > + ($i:ident ( $e:expr )) => { > + $crate::Slot::Single($crate::Function::Device($crate::Device::$i($e))) > + }; > + ($i:ident) => { > + $crate::Slot::Single($crate::Function::Device($crate::Device::$i)) > + }; > +} > + > +pub(crate) mod legacy; > + > +/// Constructs a correctly sized list of bridge slots, starting at address 0x01 > +/// because 0x00 is always taken by the bridge itself. > +/// > +/// Panics if more slots than fit are passed in. As this is a `const fn` used > +/// from constants only, that turns into a compile time error. > +pub(crate) const fn bridge_slots<'a>(slots: &'a [Slot<'a>]) -> [Slot<'a>; SLOTS_PER_BUS] { > + const PLACEHOLDER: Slot = Slot::Unused; > + let mut out = [PLACEHOLDER; SLOTS_PER_BUS]; > + if slots.len() > SLOTS_PER_BUS - 1 { > + panic!("too many devices on bridge"); > + } > + out[0] = Slot::Reserved; > + let mut i = 1; > + while i < SLOTS_PER_BUS && i - 1 < slots.len() { > + out[i] = slots[i - 1]; > + i += 1; > + } > + out > +} > + > +#[cfg(test)] > +mod test { > + use crate::Slot; > + use crate::constants::SLOTS_PER_BUS; > + use crate::layout::bridge_slots; > + > + #[test] > + fn test_bridge_slots() { > + const BRIDGE: [Slot; SLOTS_PER_BUS] = > + bridge_slots(&[Slot::Reserved, single_device!(Viommu)]); > + assert_eq!(BRIDGE[1], Slot::Reserved); > + } > +} > diff --git a/pve-qemu-server-pci/src/lib.rs b/pve-qemu-server-pci/src/lib.rs > new file mode 100644 > index 0000000..43a3886 > --- /dev/null > +++ b/pve-qemu-server-pci/src/lib.rs > @@ -0,0 +1,33 @@ > +//! Generation of the PCI and PCIe addresses of the devices of a PVE guest. > +//! > +//! Which address a device gets is part of the guest's ABI: moving a device > +//! makes the guest operating system see it as a new one, so the assignment has > +//! to stay stable for the lifetime of a VM. This crate therefore describes the > +//! assignment as a static [`layout`] and only looks addresses up in it. > +//! > +//! The entry points below reproduce what the Perl implementation in > +//! `PVE::QemuServer::PCI` does, so that both can be used interchangeably. > + > +pub mod constants; > + > +mod hardware_config; > +pub use hardware_config::HardwareConfig; > + > +mod types; > +pub use types::*; > + > +mod error; > +pub use error::LayoutError; > + > +pub mod layout; > + > +/// Returns the number of the PCI bridge `id` sits on, or `None` if it is on > +/// the root bus or not a known device. > +pub fn pci_bridge_for_device(id: &str) -> Option { > + layout::legacy::pci_bridge_for_device(id) > +} > + > +/// Returns the address of the device with the configuration id `id`. > +pub fn address(hw: &HardwareConfig, id: &str) -> Result { > + layout::legacy::address(hw, id) > +} > diff --git a/pve-qemu-server-pci/src/types/bus.rs b/pve-qemu-server-pci/src/types/bus.rs > new file mode 100644 > index 0000000..052ef00 > --- /dev/null > +++ b/pve-qemu-server-pci/src/types/bus.rs > @@ -0,0 +1,198 @@ > +#[cfg(test)] > +use strum::VariantArray; > + > +use crate::{Device, LayoutError}; > + > +/// The kinds of bridge PVE adds itself, as opposed to the ones a QEMU machine > +/// type already brings along. > +/// > +/// Each kind gets its own set of bridges so that the slot of a device only > +/// depends on its own index, not on how many devices of other kinds exist. > +#[cfg_attr(test, derive(VariantArray))] > +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)] > +pub enum PveBridge { > + Net, > + Scsi, > + VirtioBlk, > + VirtioFs, > + Hostpci, > +} > + > +impl PveBridge { > + /// The bus formed by the `index`th bridge of this kind. > + pub const fn new_bus(self, index: u8) -> Bus { > + Bus::Pve { kind: self, index } > + } > + > + /// The device this kind of bridge carries at the given device index. > + pub const fn device(self, index: u8) -> Device { > + match self { > + PveBridge::Net => Device::Net(index), > + PveBridge::Scsi => Device::ScsiController(index), > + PveBridge::VirtioBlk => Device::VirtioBlk(index), > + PveBridge::VirtioFs => Device::VirtioFs(index), > + PveBridge::Hostpci => Device::Hostpci(index), > + } > + } > +} > + > +impl std::fmt::Display for PveBridge { > + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + let text = match self { > + PveBridge::Net => "net", > + PveBridge::Scsi => "scsi", > + PveBridge::VirtioBlk => "virtio-blk", > + PveBridge::VirtioFs => "virtio-fs", > + PveBridge::Hostpci => "hostpci", > + }; > + f.write_str(text) > + } > +} > + > +impl std::str::FromStr for PveBridge { > + type Err = LayoutError; > + > + fn from_str(s: &str) -> Result { > + let kind = match s { > + "net" => PveBridge::Net, > + "scsi" => PveBridge::Scsi, > + "virtio-blk" => PveBridge::VirtioBlk, > + "virtio-fs" => PveBridge::VirtioFs, > + "hostpci" => PveBridge::Hostpci, > + _ => return Err(LayoutError::InvalidConfigId), > + }; > + Ok(kind) > + } > +} > + > +/// A bus a device can be attached to, identified the same way QEMU's `bus=` > +/// parameter does. > +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)] > +pub enum Bus { > + /// A PCI bus. Index 0 is the root bus of a non-PCIe machine. > + Pci(u8), > + /// A PCIe bus. Index 0 is the root bus of a PCIe machine. > + Pcie(u8), > + /// The bus behind a PCIe root port, which can hold a single device. > + RootPort(u8), > + /// A legacy, deprecated variant of a root port. > + #[deprecated] > + LegacyRootPort(u8), > + /// The PCI bus behind the q35 DMI-to-PCI bridge. > + Pcidmi, > + /// A bridge for the guest's built-in devices. > + Sys(u8), > + /// One of the bridges PVE adds per device kind, see [`PveBridge`]. > + Pve { kind: PveBridge, index: u8 }, > +} > + > +type BusConstructor = fn(u8) -> Bus; > + > +impl Bus { > + /// The QEMU device model needed to create this bus below `parent`, or > + /// `None` if the bus is not created by a bridge device. > + pub fn bridge_model(self, parent: Bus) -> Option<&'static str> { > + match self { > + #[expect(deprecated)] > + Bus::Pcie(_) | Bus::RootPort(_) | Bus::LegacyRootPort(_) => None, > + Bus::Pcidmi => Some("i82801b11-bridge"), > + Bus::Sys(_) | Bus::Pci(_) | Bus::Pve { .. } => Some(match parent { > + #[expect(deprecated)] > + Bus::Pcie(_) | Bus::RootPort(_) | Bus::LegacyRootPort(_) => "pcie-pci-bridge", > + _ => "pci-bridge", > + }), > + } > + } > +} > + > +impl std::fmt::Display for Bus { > + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + match self { > + Bus::Pci(i) => write!(f, "pci.{i}"), > + Bus::Pcie(i) => write!(f, "pcie.{i}"), > + Bus::RootPort(i) => write!(f, "pve.root-port{i}"), > + #[expect(deprecated)] > + Bus::LegacyRootPort(i) => write!(f, "ich9-pcie-port-{i}"), > + Bus::Sys(i) => write!(f, "pve.sys{i}"), > + Bus::Pcidmi => write!(f, "pcidmi"), > + Bus::Pve { kind, index } => { > + write!(f, "pve.{kind}{index}") > + } > + } > + } > +} > + > +impl std::str::FromStr for Bus { > + type Err = LayoutError; > + > + fn from_str(s: &str) -> Result { > + // NOTE: the prefixes here must be checked before the generic 'pve.' > + // one below, which would otherwise swallow them. > + const PREFIXES: &[(&str, BusConstructor)] = &[ > + ("pci.", Bus::Pci), > + ("pcie.", Bus::Pcie), > + ("pve.root-port", Bus::RootPort), > + #[expect(deprecated)] > + ("ich9-pcie-port-", Bus::LegacyRootPort), > + ("pve.sys", Bus::Sys), > + ]; > + > + if s == "pcidmi" { > + return Ok(Bus::Pcidmi); > + } > + > + for (prefix, c) in PREFIXES { > + if let Some(rest) = s.strip_prefix(prefix) { > + let index: u8 = rest.parse().map_err(|_| LayoutError::InvalidConfigId)?; > + return Ok(c(index)); > + } > + } > + > + if let Some(rest) = s.strip_prefix("pve.") > + && let Some(idx) = rest.find(|c: char| c.is_ascii_digit()) > + { > + let kind: PveBridge = rest[0..idx] > + .parse() > + .map_err(|_| LayoutError::InvalidConfigId)?; > + let index: u8 = rest[idx..] > + .parse() > + .map_err(|_| LayoutError::InvalidConfigId)?; > + return Ok(Bus::Pve { kind, index }); > + } > + > + Err(LayoutError::InvalidConfigId) > + } > +} > + > +#[cfg(test)] > +mod test { > + use strum::VariantArray; > + > + use crate::Bus; > + > + use super::PveBridge; > + > + #[test] > + fn test_roundtrip() { > + let test = |bus: Bus| { > + let id = bus.to_string(); > + let new_bus: Bus = id > + .parse() > + .unwrap_or_else(|_| panic!("could not parse {id}")); > + assert_eq!(bus, new_bus); > + }; > + > + test(Bus::Pcidmi); > + test(Bus::Pci(99)); > + test(Bus::Pcie(99)); > + > + for kind in PveBridge::VARIANTS.iter().copied() { > + test(Bus::Pve { kind, index: 99 }); > + } > + > + test(Bus::RootPort(99)); > + #[expect(deprecated)] > + test(Bus::LegacyRootPort(99)); > + test(Bus::Sys(99)); > + } > +} > diff --git a/pve-qemu-server-pci/src/types/device.rs b/pve-qemu-server-pci/src/types/device.rs > new file mode 100644 > index 0000000..d3bf60b > --- /dev/null > +++ b/pve-qemu-server-pci/src/types/device.rs > @@ -0,0 +1,173 @@ > +use std::{fmt::Display, str::FromStr}; > +use strum::{EnumDiscriminants, VariantArray}; > + > +use crate::{Bus, LayoutError}; > + > +/// A device that needs a PCI address. > +/// > +/// Variants carrying an index map to a configuration key of the same name plus > +/// that index, for example `Net(3)` is the guest's `net3`. > +#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq, EnumDiscriminants)] > +#[strum_discriminants(derive(VariantArray))] > +pub enum Device { > + Ahci, > + Balloon, > + Watchdog, > + GuestAgent, > + SpiceSerial, > + Rng, > + Audio, > + Ivshmem, > + Bridge(Bus), > + RootPort(Bus), > + Vga(u8), > + Viommu, > + VirtioFs(u8), > + Net(u8), > + ScsiController(u8), > + VirtioBlk(u8), > + Piix3Controller, > + XhciController(u8), > + Hostpci(u8), > + Hostpcie(u8), > + /// The EHCI USB controllers of the ICH9 chipset, numbered from one like > + /// their QEMU models. > + Ich9Ehci(u8), > + /// The UHCI USB controllers of the ICH9 chipset, numbered from one like > + /// their QEMU models. > + Ich9Uhci(u8), > + /// The HDA controller legacy q35 guests always have, independent of a > + /// configured `audio0`. > + Ich9Audio, > +} > + > +impl Display for Device { > + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + let text = match self { > + Device::Ahci => "ahci", > + Device::Balloon => "balloon", > + Device::Watchdog => "watchdog", > + Device::GuestAgent => "guest-agent", > + Device::SpiceSerial => "spice-serial", > + Device::Rng => "rng", > + Device::Audio => "audio", > + Device::Ivshmem => "ivshmem", > + Device::Viommu => "viommu", > + Device::Piix3Controller => "Piix3Controller", > + Device::Ich9Audio => "ich9-audio", > + _ => "", > + }; > + if !text.is_empty() { > + return f.write_str(text); > + } > + let text = match self { > + Device::Bridge(bus) => bus.to_string(), > + Device::RootPort(bus) => bus.to_string(), > + Device::Vga(i) => format!("vga{i}"), > + Device::VirtioFs(i) => format!("virtiofs{i}"), > + Device::Net(i) => format!("net{i}"), > + Device::ScsiController(i) => format!("scsihw{i}"), > + Device::VirtioBlk(i) => format!("virtioblk{i}"), > + Device::XhciController(i) => format!("xhci{i}"), > + Device::Hostpci(i) => format!("hostpci{i}"), > + Device::Hostpcie(i) => format!("hostpcie{i}"), > + Device::Ich9Ehci(i) => format!("ich9-ehci{i}"), > + Device::Ich9Uhci(i) => format!("ich9-uhci{i}"), > + _ => String::new(), > + }; > + write!(f, "{text}") > + } > +} > + > +/// Rebuilds the single possible [`Device`] of a discriminant, which only works > +/// for the variants that carry no index. > +impl TryFrom for Device { > + type Error = LayoutError; > + > + fn try_from(value: DeviceDiscriminants) -> Result { > + let res = match value { > + DeviceDiscriminants::Ahci => Device::Ahci, > + DeviceDiscriminants::Balloon => Device::Balloon, > + DeviceDiscriminants::Watchdog => Device::Watchdog, > + DeviceDiscriminants::GuestAgent => Device::GuestAgent, > + DeviceDiscriminants::SpiceSerial => Device::SpiceSerial, > + DeviceDiscriminants::Rng => Device::Rng, > + DeviceDiscriminants::Audio => Device::Audio, > + DeviceDiscriminants::Ivshmem => Device::Ivshmem, > + DeviceDiscriminants::Piix3Controller => Device::Piix3Controller, > + DeviceDiscriminants::Ich9Audio => Device::Ich9Audio, > + DeviceDiscriminants::Bridge > + | DeviceDiscriminants::RootPort > + | DeviceDiscriminants::Vga > + | DeviceDiscriminants::Viommu > + | DeviceDiscriminants::VirtioFs > + | DeviceDiscriminants::Net > + | DeviceDiscriminants::ScsiController > + | DeviceDiscriminants::VirtioBlk > + | DeviceDiscriminants::XhciController > + | DeviceDiscriminants::Hostpci > + | DeviceDiscriminants::Hostpcie > + | DeviceDiscriminants::Ich9Ehci > + | DeviceDiscriminants::Ich9Uhci => return Err(LayoutError::InvalidConfigId), > + }; > + Ok(res) > + } > +} > + > +type DeviceConstructor = fn(u8) -> Device; > + > +/// Parses the configuration id of a device, for example `net0` or `hostpci3`. > +impl FromStr for Device { > + type Err = LayoutError; > + > + fn from_str(s: &str) -> Result { > + // NOTE: order matters, the longer prefixes have to come first so that > + // 'virtio' does not shadow 'virtioscsi' and 'hostpci' not 'hostpcie'. > + const PREFIXES: &[(&str, DeviceConstructor)] = &[ > + ("net", Device::Net), > + ("scsihw", Device::ScsiController), > + ("virtioscsi", Device::ScsiController), > + ("virtiofs", Device::VirtioFs), > + ("virtio", Device::VirtioBlk), > + ("hostpcie", Device::Hostpcie), > + ("hostpci", Device::Hostpci), > + ]; > + > + for (prefix, c) in PREFIXES { > + if let Some(index) = s.strip_prefix(prefix) { > + let index = index.parse().map_err(|_| LayoutError::InvalidConfigId)?; > + return Ok(c(index)); > + } > + } > + > + let class = match s { > + "viommu" => Device::Viommu, > + "vga" => Device::Vga(0), > + "vga1" => Device::Vga(1), > + "vga2" => Device::Vga(2), > + "vga3" => Device::Vga(3), > + "xhci" => Device::XhciController(0), > + "ahci0" => Device::Ahci, > + "balloon0" => Device::Balloon, > + "watchdog" => Device::Watchdog, > + "qga0" => Device::GuestAgent, > + "spice" => Device::SpiceSerial, > + "rng0" => Device::Rng, > + "audio0" => Device::Audio, > + "ivshmem" => Device::Ivshmem, > + // legacy rules > + "ehci" | "piix3" => Device::Piix3Controller, > + "legacy-igd" => Device::Vga(0), > + "pci.2-igd" => Device::Bridge(Bus::Pci(99)), > + other => { > + if let Ok(bridge) = other.parse() { > + return Ok(Device::Bridge(bridge)); > + } > + > + return Err(LayoutError::InvalidConfigId); > + } > + }; > + > + Ok(class) > + } > +} > diff --git a/pve-qemu-server-pci/src/types/layout.rs b/pve-qemu-server-pci/src/types/layout.rs > new file mode 100644 > index 0000000..889b961 > --- /dev/null > +++ b/pve-qemu-server-pci/src/types/layout.rs > @@ -0,0 +1,329 @@ > +use std::collections::VecDeque; > + > +use crate::constants::{FUNCTIONS_PER_DEVICE, SLOTS_PER_BUS, USABLE_SLOTS_PER_BUS}; > +use crate::{Bus, Device, LayoutError, PciAddress, PveBridge}; > + > +/// What sits at one function number of a slot. > +#[derive(Clone, Copy, Debug, PartialEq, Eq)] > +pub enum Function<'a> { > + /// Occupied by something outside of this crate's control, for example by > + /// the machine type's own configuration file. > + Reserved, > + /// Free for future use. Kept explicit so that the surrounding entries keep > + /// their address when something is added later. > + Unused, > + Device(Device), > + /// A bridge with a statically known set of slots behind it. > + FixedBridge(Bus, &'a [Slot<'a>; SLOTS_PER_BUS]), > + /// A PCIe root port with the single device attached to it. > + RootPort(Bus, Device), > +} > + > +/// What sits at one slot of a bus. > +#[derive(Clone, Copy, Debug, PartialEq, Eq)] > +pub enum Slot<'a> { > + /// Occupied by something outside of this crate's control. > + Reserved, > + /// Free for future use, see [`Function::Unused`]. > + Unused, > + /// A single device on function 0. > + Single(Function<'a>), > + /// A multifunction slot with one entry per function number. > + Multi(&'a [Function<'a>; FUNCTIONS_PER_DEVICE]), > + /// As many bridges of `kind` as are needed to hold `max` devices. The > + /// bridges share this slot by using one function number each. > + DynamicBridges { kind: PveBridge, max: u8 }, > +} > + > +/// The complete slot assignment of a guest, starting at the root bus. > +/// > +/// A layout is a static description: it lists every address the crate can > +/// hand out, regardless of which devices a concrete guest configures. > +#[derive(Clone, Copy, Debug, PartialEq)] > +pub struct Layout<'a> { > + /// Whether the root bus is PCIe. Only the root bus is affected, nested > + /// buses stay plain PCI. > + pub pcie: bool, > + pub root: [Slot<'a>; SLOTS_PER_BUS], > +} > + > +impl Layout<'_> { > + /// Looks up the address of a device in this layout. > + /// > + /// The first match wins, so the order of the layout is significant: a > + /// device may legitimately appear in two places, for example a SCSI > + /// controller that moves to its own bridge with `virtio-scsi-single`. > + pub fn find_device(&self, device: &Device) -> Result { > + self.iter() > + .find_map(|used| (used.device == *device).then_some(used.addr)) > + .ok_or(LayoutError::NoAddressFound(*device)) > + } > + > + /// Iterates over every address this layout hands out. > + /// > + /// Bridges are yielded before the devices behind them, so the result can > + /// be turned into a command line in order. > + pub fn iter(&self) -> AddressAssignments<'_> { > + AddressAssignments::new(self) > + } > +} > + > +/// The address a layout assigns to a device. > +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] > +pub struct AddressAssignment { > + pub addr: PciAddress, > + pub device: Device, > + /// Whether QEMU has to be told that other functions of this slot are in > + /// use. Only ever set on function 0. > + pub multifunction: bool, > +} > + > +/// Iterator over the address assignments of a [`Layout`], see > +/// [`Layout::iter`]. > +#[derive(Debug)] > +pub struct AddressAssignments<'a> { > + resolved: VecDeque, > + bridge_slot_stack: Vec<(Slot<'a>, PciAddress)>, > +} > + > +impl<'a> AddressAssignments<'a> { > + pub(crate) fn new(layout: &Layout<'a>) -> Self { > + let bus_type = if layout.pcie { > + Bus::Pcie(0) > + } else { > + Bus::Pci(0) > + }; > + let mut stack: Vec<(Slot<'a>, PciAddress)> = Vec::new(); > + for (device, slot) in layout.root.iter().enumerate().rev() { > + let address = PciAddress::new(bus_type, device as u8, 0); > + stack.push((*slot, address)); > + } > + Self { > + bridge_slot_stack: stack, > + resolved: VecDeque::new(), > + } > + } > + > + /// Resolves one function of a slot into the queue of address assignments. > + /// > + /// A bridge is queued before the devices behind it: fixed bridges push > + /// their slots onto the stack, which is only drained once the queue is > + /// empty again. > + fn handle_function(&mut self, addr: PciAddress, function: Function<'a>, multifunction: bool) { > + match function { > + Function::Reserved | Function::Unused => {} > + Function::Device(device) => { > + self.resolved.push_back(AddressAssignment { > + addr, > + device, > + multifunction, > + }); > + } > + Function::RootPort(bus, device) => { > + self.resolved.push_back(AddressAssignment { > + addr, > + device: Device::RootPort(bus), > + multifunction, > + }); > + self.resolved.push_back(AddressAssignment { > + addr: PciAddress::new(bus, 0, 0), > + device, > + multifunction: false, > + }); > + } > + Function::FixedBridge(bus_type, slots) => { > + self.resolved.push_back(AddressAssignment { > + addr, > + device: Device::Bridge(bus_type), > + multifunction, > + }); > + for (device, slot) in slots.iter().enumerate().rev() { > + let addr = PciAddress::new(bus_type, device as u8, 0); > + self.bridge_slot_stack.push((*slot, addr)); > + } > + } > + } > + } > + > + /// Resolves a slot of dynamic bridges into the queue of address > + /// assignments. > + /// > + /// Unlike fixed bridges, the devices behind them are queued right away, > + /// as they are generated from the bridge kind rather than listed in the > + /// layout. > + fn handle_dynamic_bridges(&mut self, mut addr: PciAddress, kind: PveBridge, max: u8) { > + // the bridges share one slot, so there cannot be more of them than the > + // slot has function numbers > + let num_bridges = max.div_ceil(USABLE_SLOTS_PER_BUS); > + debug_assert!(usize::from(num_bridges) <= FUNCTIONS_PER_DEVICE); > + > + for bridge_index in 0..num_bridges { > + addr.function = bridge_index; > + let bus = kind.new_bus(bridge_index); > + self.resolved.push_back(AddressAssignment { > + addr, > + device: Device::Bridge(bus), > + multifunction: bridge_index == 0 && num_bridges > 1, > + }); > + for slot_nr in 1..=USABLE_SLOTS_PER_BUS { > + // u16 so that a bigger 'max' cannot overflow > + let index = u16::from(bridge_index) * u16::from(USABLE_SLOTS_PER_BUS) > + + u16::from(slot_nr) > + - 1; > + if index >= u16::from(max) { > + break; > + } > + self.resolved.push_back(AddressAssignment { > + addr: PciAddress::new(bus, slot_nr, 0), > + device: kind.device(index as u8), > + multifunction: false, > + }); > + } > + } > + } > +} > + > +impl Iterator for AddressAssignments<'_> { > + type Item = AddressAssignment; > + > + fn next(&mut self) -> Option { > + loop { > + if let Some(assignment) = self.resolved.pop_front() { > + return Some(assignment); > + } > + if let Some((slot, mut addr)) = self.bridge_slot_stack.pop() { > + match slot { > + Slot::Reserved | Slot::Unused => {} > + Slot::Single(function) => self.handle_function(addr, function, false), > + Slot::Multi(functions) => { > + for (function_nr, function) in functions.iter().enumerate() { > + addr.function = function_nr as u8; > + self.handle_function(addr, *function, function_nr == 0); > + } > + } > + Slot::DynamicBridges { kind, max } => { > + self.handle_dynamic_bridges(addr, kind, max) > + } > + } > + } > + > + if self.resolved.is_empty() && self.bridge_slot_stack.is_empty() { > + return None; > + } > + } > + } > +} > + > +#[cfg(test)] > +mod test { > + use crate::constants::SLOTS_PER_BUS; > + use crate::layout::bridge_slots; > + use crate::{AddressAssignment, Bus, Device, PciAddress}; > + > + use super::{Function, Layout, Slot}; > + > + const BRIDGE: [Slot; SLOTS_PER_BUS] = > + bridge_slots(&[Slot::Single(Function::Device(Device::Watchdog))]); > + const LAYOUT: Layout<'static> = Layout { > + pcie: false, > + root: crate::layout::bridge_slots(&[ > + Slot::Single(Function::Device(Device::Rng)), > + Slot::Single(Function::Device(Device::Audio)), > + Slot::Reserved, > + Slot::Unused, > + Slot::Multi(&[ > + Function::Reserved, > + Function::Unused, > + Function::Unused, > + Function::Unused, > + Function::Device(Device::Viommu), > + Function::Unused, > + Function::Unused, > + Function::Unused, > + ]), > + Slot::Single(Function::FixedBridge(Bus::Pci(99), &BRIDGE)), > + ]), > + }; > + > + #[test] > + fn basic_layout() { > + let test = |device: Device, addr: PciAddress| { > + let found = LAYOUT > + .find_device(&device) > + .unwrap_or_else(|_| panic!("could not find {device:?}")); > + assert_eq!(found, addr); > + }; > + > + test(Device::Rng, PciAddress::new(Bus::Pci(0), 1, 0)); > + test(Device::Audio, PciAddress::new(Bus::Pci(0), 2, 0)); > + test(Device::Viommu, PciAddress::new(Bus::Pci(0), 5, 4)); > + test(Device::Watchdog, PciAddress::new(Bus::Pci(99), 1, 0)); > + } > + > + #[test] > + fn iterator() { > + let mut iter = LAYOUT.iter(); > + let pci0 = Bus::Pci(0); > + assert_eq!( > + iter.next(), > + Some(AddressAssignment { > + addr: PciAddress { > + bus: pci0, > + device: 1, > + function: 0 > + }, > + device: Device::Rng, > + multifunction: false, > + }) > + ); > + assert_eq!( > + iter.next(), > + Some(AddressAssignment { > + addr: PciAddress { > + bus: pci0, > + device: 2, > + function: 0 > + }, > + device: Device::Audio, > + multifunction: false, > + }) > + ); > + assert_eq!( > + iter.next(), > + Some(AddressAssignment { > + addr: PciAddress { > + bus: pci0, > + device: 5, > + function: 4 > + }, > + device: Device::Viommu, > + multifunction: false, > + }) > + ); > + assert_eq!( > + iter.next(), > + Some(AddressAssignment { > + addr: PciAddress { > + bus: pci0, > + device: 6, > + function: 0 > + }, > + device: Device::Bridge(Bus::Pci(99)), > + multifunction: false, > + }) > + ); > + assert_eq!( > + iter.next(), > + Some(AddressAssignment { > + addr: PciAddress { > + bus: Bus::Pci(99), > + device: 1, > + function: 0 > + }, > + device: Device::Watchdog, > + multifunction: false, > + }) > + ); > + assert_eq!(iter.next(), None); > + } > +} > diff --git a/pve-qemu-server-pci/src/types/mod.rs b/pve-qemu-server-pci/src/types/mod.rs > new file mode 100644 > index 0000000..3847745 > --- /dev/null > +++ b/pve-qemu-server-pci/src/types/mod.rs > @@ -0,0 +1,11 @@ > +mod bus; > +pub use bus::{Bus, PveBridge}; > + > +mod device; > +pub use device::Device; > + > +mod pci_address; > +pub use pci_address::PciAddress; > + > +mod layout; > +pub use layout::{AddressAssignment, AddressAssignments, Function, Layout, Slot}; > diff --git a/pve-qemu-server-pci/src/types/pci_address.rs b/pve-qemu-server-pci/src/types/pci_address.rs > new file mode 100644 > index 0000000..dcef298 > --- /dev/null > +++ b/pve-qemu-server-pci/src/types/pci_address.rs > @@ -0,0 +1,36 @@ > +use crate::Bus; > + > +/// The complete address of a device: the bus it sits on plus the slot and > +/// function number within that bus. > +#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq)] > +pub struct PciAddress { > + pub bus: Bus, > + pub device: u8, > + pub function: u8, > +} > + > +impl PciAddress { > + pub const fn new(bus: Bus, device: u8, function: u8) -> Self { > + Self { > + bus, > + device, > + function, > + } > + } > + > + /// Renders the address as the `,bus=...,addr=...` suffix of a QEMU > + /// `-device` argument, including the leading comma. > + /// > + /// The function number is only emitted when it is not zero, matching what > + /// the Perl implementation produces. > + pub fn to_qemu_addr(&self) -> String { > + let bus = self.bus; > + let function = if self.function > 0 { > + format!(".{}", self.function) > + } else { > + String::new() > + }; > + > + format!(",bus={bus},addr=0x{:x}{function}", self.device) > + } > +} > diff --git a/rustfmt.toml b/rustfmt.toml > new file mode 100644 > index 0000000..f3e454b > --- /dev/null > +++ b/rustfmt.toml > @@ -0,0 +1,2 @@ > +edition = "2024" > +style_edition = "2024" > -- > 2.47.3 > > > > > --