using RackPeek.Domain.Api;
using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// Proxmox's side of the MAC bridge: guest configs carry the NIC MACs Proxmox
/// assigned, a network scan sees exactly those MACs on the wire, and the resolver
/// lands both collectors on one card — the same contract the system collector has.
///
public class ProxmoxMacBridgeTests {
// -- parsing ------------------------------------------------------------------------
[Theory]
[InlineData("pve-qemu-config.json", "bc:24:11:12:34:56")] // virtio=BC:24:11:…
[InlineData("pve-lxc-config.json", "bc:24:11:aa:bb:cc")] // hwaddr=BC:24:11:…
[InlineData("pve-lxc-config-dhcp.json", "bc:24:11:dd:ee:ff")] // dhcp still has a MAC
public void A_guests_config_yields_its_normalised_mac(string fixture, string expected) {
ProxmoxGuestConfig config = ProxmoxResponseParser.ParseGuestConfig(Fixture.Read(fixture));
Assert.Equal([expected], config.Macs);
}
[Theory]
[InlineData("""{"data":{}}""")]
[InlineData("""{"data":{"ostype":"l26","scsi0":"local-lvm:vm-1-disk-0,size=64G"}}""")]
[InlineData("""{"data":{"net0":"bridge=vmbr0,firewall=1"}}""")] // a net line with no MAC
[InlineData("""{"data":{"network":"virtio=BC:24:11:12:34:56"}}""")] // not a netN slot
public void A_config_without_nic_macs_yields_none(string json) {
ProxmoxGuestConfig config = ProxmoxResponseParser.ParseGuestConfig(json);
Assert.Empty(config.Macs ?? []);
}
[Fact]
public void Every_nic_of_a_multi_homed_guest_is_recorded() {
ProxmoxGuestConfig config = ProxmoxResponseParser.ParseGuestConfig(
"""
{"data":{
"net0":"virtio=BC:24:11:12:34:56,bridge=vmbr0",
"net1":"e1000=BC:24:11:99:88:77,bridge=vmbr1,tag=50"
}}
""");
Assert.Equal(["bc:24:11:12:34:56", "bc:24:11:99:88:77"], config.Macs);
}
// -- mapping ------------------------------------------------------------------------
[Fact]
public void A_guest_card_carries_its_macs_label() {
List resources = ProxmoxResourceMapper.ToResources(
"homelab",
[new ProxmoxNode { Name = "pve01" }],
[
new ProxmoxGuest {
VmId = 104,
Node = "pve01",
Name = "docker-01",
Type = "vm",
Macs = ["bc:24:11:12:34:56"]
}
]);
SystemResource guest = resources.OfType().Single(r => r.Name == "docker-01");
Assert.Equal("bc:24:11:12:34:56", guest.Labels["macs"]);
}
// -- the bridge, end to end through the real server ----------------------------------
[Fact]
public async Task A_scanned_guest_and_its_proxmox_card_become_one() {
// The sweep found the VM on the LAN first — by the very MAC Proxmox assigned it.
var scanned = DiscoveryDocument.ToYaml(NetworkScanMapper.ToResources([
new NetworkHostFact("192.168.1.178", "bc:24:11:12:34:56", null, true, [])
]));
using var api = new DiscoveryApiFixture(scanned);
// Now `rpk discover proxmox` reports the estate, including that guest.
List estate = ProxmoxResourceMapper.ToResources(
"homelab",
[new ProxmoxNode { Name = "pve01" }],
[
new ProxmoxGuest {
VmId = 104,
Node = "pve01",
Name = "docker-01",
Type = "vm",
Cores = 4,
Os = "Linux",
Macs = ["bc:24:11:12:34:56"]
}
]);
ImportYamlResponse response = await api.PublishAsync(DiscoveryDocument.ToYaml(estate));
var stored = api.StoredYaml;
// The guest landed on the scan's card: identity upgraded to the vmid-based id,
// the scan's address kept, no duplicate for the same machine.
Assert.DoesNotContain("rpk1:net:", stored);
Assert.Contains("rpk1:pve:", stored);
Assert.Contains("ip: 192.168.1.178", stored);
Assert.Contains("os: Linux", stored);
Assert.DoesNotContain(response.Added, name => name.StartsWith("docker-01"));
Fixture.AssertConformsToSchema(stored);
// And a rescan afterwards still lands on that same card via the MAC.
ImportYamlResponse rescan = await api.PublishAsync(DiscoveryDocument.ToYaml(
NetworkScanMapper.ToResources([
new NetworkHostFact("192.168.1.179", "bc:24:11:12:34:56", null, true, [])
])));
Assert.Empty(rescan.Added);
Assert.Contains("rpk1:pve:", api.StoredYaml);
Assert.Contains("ip: 192.168.1.179", api.StoredYaml);
}
}