Merge fix/guest-agent-ip-wait: correct guest-agent REST path + skip lo
This commit is contained in:
@@ -537,20 +537,13 @@ class ProxmoxVMProvisionMixin:
|
|||||||
vmid_int = int(vmid)
|
vmid_int = int(vmid)
|
||||||
_logger.debug(f"Getting status for VM {vmid}")
|
_logger.debug(f"Getting status for VM {vmid}")
|
||||||
|
|
||||||
# Get VM config to infer net0 MAC (for matching guest-agent results)
|
# VM must exist / be readable before we start polling.
|
||||||
try:
|
try:
|
||||||
config = self._node_api().qemu(vmid_int).config.get()
|
self._node_api().qemu(vmid_int).config.get()
|
||||||
except Exception:
|
except Exception:
|
||||||
# VM may not exist yet or config not readable
|
# VM may not exist yet or config not readable
|
||||||
return {"status": "unknown"}
|
return {"status": "unknown"}
|
||||||
|
|
||||||
# Parse net0 MAC from config (if present)
|
|
||||||
net0_line = config.get("net0", "")
|
|
||||||
expected_mac = None
|
|
||||||
# Example: "virtio,bridge=vmbr0,tag=10" — no explicit MAC
|
|
||||||
# Proxmox auto-generates MACs in a deterministic pattern, but we'll
|
|
||||||
# match by looking for the first NIC's IP in guest-agent results
|
|
||||||
|
|
||||||
# Polling loop
|
# Polling loop
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while True:
|
while True:
|
||||||
@@ -559,27 +552,34 @@ class ProxmoxVMProvisionMixin:
|
|||||||
raise RuntimeError(f"VM {vmid} failed to acquire IP within {timeout}s")
|
raise RuntimeError(f"VM {vmid} failed to acquire IP within {timeout}s")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Query guest-agent network interfaces
|
# Query guest-agent network interfaces. Proxmox's REST path is
|
||||||
agent_info = self._node_api().qemu(vmid_int).agent.network_get_interfaces.get()
|
# "network-get-interfaces" (hyphens) — it must be passed as a
|
||||||
interfaces = agent_info.get("result", [])
|
# resource id via __call__, not dotted attribute access (which
|
||||||
|
# would silently build a non-existent "network_get_interfaces"
|
||||||
|
# path and 404 on every poll).
|
||||||
|
agent_info = (
|
||||||
|
self._node_api().qemu(vmid_int).agent("network-get-interfaces").get()
|
||||||
|
)
|
||||||
|
interfaces = (agent_info or {}).get("result", [])
|
||||||
|
|
||||||
# Find net0 (first interface with IP)
|
# The guest agent does not report interfaces in a fixed order —
|
||||||
if interfaces:
|
# "lo" commonly comes first. Skip it and take the first real
|
||||||
net0_iface = interfaces[0] # Assumes net0 is first in list
|
# NIC that has an IPv4 address.
|
||||||
net0_mac = net0_iface.get("hardware-address", "")
|
for iface in interfaces:
|
||||||
ip_addresses = net0_iface.get("ip-addresses", [])
|
name = iface.get("name", "")
|
||||||
|
if not name or name == "lo":
|
||||||
if ip_addresses:
|
continue
|
||||||
# Found IP
|
for addr in iface.get("ip-addresses", []):
|
||||||
ip_info = ip_addresses[0]
|
if addr.get("ip-address-type") != "ipv4":
|
||||||
ip_addr = ip_info.get("ip-address", "")
|
continue
|
||||||
|
ip_addr = addr.get("ip-address", "")
|
||||||
if ip_addr:
|
if ip_addr:
|
||||||
_logger.info(f"VM {vmid} acquired IP {ip_addr}")
|
_logger.info(f"VM {vmid} acquired IP {ip_addr}")
|
||||||
return {
|
return {
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"ip_address": ip_addr,
|
"ip_address": ip_addr,
|
||||||
"hostname": net0_iface.get("name", ""),
|
"hostname": name,
|
||||||
"mac_address": net0_mac,
|
"mac_address": iface.get("hardware-address", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -335,9 +335,12 @@ def test_get_vm_status_with_wait_for_ip():
|
|||||||
mock_node.qemu.return_value = mock_vm
|
mock_node.qemu.return_value = mock_vm
|
||||||
mock_vm.config.get.return_value = {"net0": "virtio,bridge=vmbr0,tag=10"}
|
mock_vm.config.get.return_value = {"net0": "virtio,bridge=vmbr0,tag=10"}
|
||||||
|
|
||||||
# Mock guest-agent: first no IP, then with IP
|
# The real Proxmox REST path is "network-get-interfaces" (hyphens),
|
||||||
mock_agent = MagicMock()
|
# reached via agent(...) as a callable resource, not dotted attribute
|
||||||
mock_agent.network_get_interfaces.get.side_effect = [
|
# access — mock that exact call shape.
|
||||||
|
mock_agent_call = MagicMock()
|
||||||
|
mock_vm.agent.return_value = mock_agent_call
|
||||||
|
mock_agent_call.get.side_effect = [
|
||||||
{"result": [{"name": "eth0", "hardware-address": "aa:bb:cc:dd:ee:00"}]},
|
{"result": [{"name": "eth0", "hardware-address": "aa:bb:cc:dd:ee:00"}]},
|
||||||
{
|
{
|
||||||
"result": [
|
"result": [
|
||||||
@@ -349,16 +352,51 @@ def test_get_vm_status_with_wait_for_ip():
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
mock_vm.agent = mock_agent
|
|
||||||
|
|
||||||
with patch("time.sleep"):
|
with patch("time.sleep"):
|
||||||
result = mixin.get_vm_status("101", wait_for_ip=True, timeout=30, poll_interval=1)
|
result = mixin.get_vm_status("101", wait_for_ip=True, timeout=30, poll_interval=1)
|
||||||
|
|
||||||
|
mock_vm.agent.assert_called_with("network-get-interfaces")
|
||||||
assert result["status"] == "running"
|
assert result["status"] == "running"
|
||||||
assert result["ip_address"] == "10.0.0.100"
|
assert result["ip_address"] == "10.0.0.100"
|
||||||
assert result["mac_address"] == "aa:bb:cc:dd:ee:00"
|
assert result["mac_address"] == "aa:bb:cc:dd:ee:00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_vm_status_skips_loopback_interface():
|
||||||
|
"""The guest agent commonly reports "lo" before the real NIC — it must
|
||||||
|
be skipped rather than mistaken for the VM's address."""
|
||||||
|
mixin = ProxmoxVMProvisionMixin()
|
||||||
|
|
||||||
|
mock_node = MagicMock()
|
||||||
|
mixin._node_api = MagicMock(return_value=mock_node)
|
||||||
|
|
||||||
|
mock_vm = MagicMock()
|
||||||
|
mock_node.qemu.return_value = mock_vm
|
||||||
|
mock_vm.config.get.return_value = {"net0": "virtio,bridge=vmbr0"}
|
||||||
|
|
||||||
|
mock_agent_call = MagicMock()
|
||||||
|
mock_vm.agent.return_value = mock_agent_call
|
||||||
|
mock_agent_call.get.return_value = {
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"name": "lo",
|
||||||
|
"hardware-address": "00:00:00:00:00:00",
|
||||||
|
"ip-addresses": [{"ip-address": "127.0.0.1", "ip-address-type": "ipv4"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eth0",
|
||||||
|
"hardware-address": "aa:bb:cc:dd:ee:00",
|
||||||
|
"ip-addresses": [{"ip-address": "10.0.0.101", "ip-address-type": "ipv4"}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("time.sleep"):
|
||||||
|
result = mixin.get_vm_status("101", wait_for_ip=True, timeout=30, poll_interval=1)
|
||||||
|
|
||||||
|
assert result["ip_address"] == "10.0.0.101"
|
||||||
|
|
||||||
|
|
||||||
def test_get_vm_status_timeout_waiting_for_ip():
|
def test_get_vm_status_timeout_waiting_for_ip():
|
||||||
"""get_vm_status raises RuntimeError if IP acquisition times out."""
|
"""get_vm_status raises RuntimeError if IP acquisition times out."""
|
||||||
mixin = ProxmoxVMProvisionMixin()
|
mixin = ProxmoxVMProvisionMixin()
|
||||||
@@ -372,11 +410,11 @@ def test_get_vm_status_timeout_waiting_for_ip():
|
|||||||
mock_vm.config.get.return_value = {"net0": "virtio,bridge=vmbr0"}
|
mock_vm.config.get.return_value = {"net0": "virtio,bridge=vmbr0"}
|
||||||
|
|
||||||
# Mock guest-agent that never returns IP
|
# Mock guest-agent that never returns IP
|
||||||
mock_agent = MagicMock()
|
mock_agent_call = MagicMock()
|
||||||
mock_agent.network_get_interfaces.get.return_value = {
|
mock_vm.agent.return_value = mock_agent_call
|
||||||
|
mock_agent_call.get.return_value = {
|
||||||
"result": [{"name": "eth0", "hardware-address": "aa:bb:cc:dd:ee:00"}]
|
"result": [{"name": "eth0", "hardware-address": "aa:bb:cc:dd:ee:00"}]
|
||||||
}
|
}
|
||||||
mock_vm.agent = mock_agent
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="timeout|IP"):
|
with pytest.raises(RuntimeError, match="timeout|IP"):
|
||||||
with patch("time.sleep"):
|
with patch("time.sleep"):
|
||||||
|
|||||||
Reference in New Issue
Block a user