Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/ProxmoxSharp/Vm/QemuParamEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,20 @@ public static string EncodeNet(QemuNet n)
return string.Join(",", opts);
}

/// <summary>hostpci0 value: "&lt;host&gt;[,pcie=1][,x-vga=1][,rombar=0][,romfile=…][,mdev=…]".</summary>
/// <summary>
/// hostpci0 value: "&lt;mapping=NAME|host&gt;[,pcie=1][,x-vga=1][,rombar=0][,romfile=…][,mdev=…]".
/// Prefers the resource-mapping form ("mapping=NAME") — the only form a token can set.
/// </summary>
public static string EncodeHostPci(QemuHostPci h)
{
ArgumentNullException.ThrowIfNull(h);
if (string.IsNullOrEmpty(h.Host)) throw new ArgumentException($"hostpci '{h.Id}' needs a Host PCI address.");
var opts = new List<string> { h.Host };
var head = (h.Mapping, h.Host) switch
{
({ } m, _) when !string.IsNullOrEmpty(m) => $"mapping={m}",
(_, { } host) when !string.IsNullOrEmpty(host) => host,
_ => throw new ArgumentException($"hostpci '{h.Id}' needs a Mapping name or a raw Host PCI address."),
};
var opts = new List<string> { head };
if (h.Pcie is true) opts.Add("pcie=1");
if (h.XVga is true) opts.Add("x-vga=1");
if (h.Rombar is false) opts.Add("rombar=0"); // only emit when explicitly disabled (default is on)
Expand Down
14 changes: 12 additions & 2 deletions src/ProxmoxSharp/Vm/QemuVmSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,21 @@ public sealed record QemuNet
public bool? Firewall { get; init; }
}

/// <summary>A PCI(e) passthrough device (hostpciN) — the gaming GPU. <see cref="Id"/> e.g. "hostpci0".</summary>
/// <summary>
/// A PCI(e) passthrough device (hostpciN) — the gaming GPU. <see cref="Id"/> e.g. "hostpci0".
/// <para>
/// Provide EITHER <see cref="Mapping"/> (a Proxmox PCI resource-mapping name) or
/// <see cref="Host"/> (a raw PCI address). Prefer <see cref="Mapping"/>: Proxmox only lets
/// the real <c>root@pam</c> user set a raw <c>hostpciN</c> ("only root can set 'hostpciN'
/// config for non-mapped devices"), whereas a mapped device is settable by any token with
/// <c>Mapping.Use</c> — and it's node-portable.
/// </para>
/// </summary>
public sealed record QemuHostPci
{
public required string Id { get; init; }
public required string Host { get; init; } // PCI address, e.g. "0000:09:00"
public string? Mapping { get; init; } // PCI resource-mapping name (token-settable; preferred)
public string? Host { get; init; } // raw PCI address, e.g. "0000:09:00" (root@pam only)
public bool? Pcie { get; init; }
public bool? XVga { get; init; }
public bool? Rombar { get; init; }
Expand Down
17 changes: 17 additions & 0 deletions tests/ProxmoxSharp.Tests/QemuParamEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ public void HostPci_encodes_the_passthrough_line_like_the_working_win_vm()
Assert.Equal("0000:09:00,pcie=1,x-vga=1", v);
}

[Fact]
public void HostPci_encodes_the_resource_mapping_form()
{
// The token-settable form: hostpci0: mapping=<name>,pcie=1,x-vga=1
var v = QemuParamEncoder.EncodeHostPci(new QemuHostPci
{
Id = "hostpci0", Mapping = "AMD_Radeon_RX6600", Pcie = true, XVga = true,
});
Assert.Equal("mapping=AMD_Radeon_RX6600,pcie=1,x-vga=1", v);
}

[Fact]
public void HostPci_without_mapping_or_host_throws()
{
Assert.Throws<ArgumentException>(() => QemuParamEncoder.EncodeHostPci(new QemuHostPci { Id = "hostpci0" }));
}

[Fact]
public void HostPci_omits_rombar_unless_explicitly_disabled()
{
Expand Down
Loading