diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a697b..2068564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- `validate` now accepts SDN VNets applied on the target node, via `/nodes/{node}/network?type=any_bridge`. The same call also rejects interfaces a VM cannot attach to (`eth0`, `bond0`, `vmbr0.50`). + ## 3.2.0 ### Added @@ -12,7 +18,6 @@ ### Added - **Console copy devices**: every VM is created with `vga: std,clipboard=vnc` and `serial0: socket`. The node's console offers its configure link to the viewer's clipboard (the noVNC clipboard panel) and writes it to the serial port (the xterm.js console). VNC clipboard needs PVE 8.2, below the 8.4.1 minimum. - ## 3.0.0 ### Breaking Changes diff --git a/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py b/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py index 7847e40..b3ac341 100644 --- a/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +++ b/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py @@ -31,7 +31,7 @@ required: true type: str network: - description: Network bridge to validate. + description: Network bridge to validate. A Linux or OVS bridge, or an SDN VNet applied on the node. required: true type: str iso_name: diff --git a/docs/configuration.md b/docs/configuration.md index 0f1c282..a2f2072 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -199,7 +199,7 @@ VM network configuration. Defaults to DHCP if omitted. |---|---|---|---|---| | `ip_allocation` | string | no | `dhcp` | `dhcp` or `static` | | `address_config` | object | if static | | Static IP configuration (see below) | -| `vlan` | integer | no | | VLAN tag (1-4095) | +| `vlan` | integer | no | | VLAN tag (1-4095). Leave unset when the bridge is an SDN VNet in a VLAN zone: the host applies the tag, and a second tag from the guest leaves the VM with no network | | `rate_limit` | integer | no | | Network rate limit. Must be 35, 75, 135, or 250 | **address_config** (required when `ip_allocation: static`): diff --git a/src/arcane_mage/provisioner.py b/src/arcane_mage/provisioner.py index 40e2f5b..34af21d 100644 --- a/src/arcane_mage/provisioner.py +++ b/src/arcane_mage/provisioner.py @@ -236,7 +236,8 @@ async def validate_iso_version(self, node: str, iso_name: str, storage_iso: str) return any(x["content"] == "iso" and x["volid"].endswith(iso_name) for x in res.payload) async def validate_network(self, node: str, network: str) -> bool: - """Validate that the specified network bridge exists on the hypervisor.""" + """Whether `network` is a bridge a VM on `node` can attach to: a Linux or + OVS bridge, or an SDN VNet applied on that node.""" res = await self.api.get_networks(node) if not res: diff --git a/src/arcane_mage/proxmox.py b/src/arcane_mage/proxmox.py index 2de7630..f423bd7 100644 --- a/src/arcane_mage/proxmox.py +++ b/src/arcane_mage/proxmox.py @@ -412,7 +412,8 @@ async def get_hypervisor_nodes(self) -> ApiResponse[list[HypervisorNode]]: return res async def get_networks(self, node: str) -> ApiResponse[list[NetworkIface]]: - endpoint = f"nodes/{node}/network" + """The bridges on `node` a VM can attach to, SDN VNets applied there included.""" + endpoint = f"nodes/{node}/network?type=any_bridge" res = await self._do_get(endpoint, _NETWORK_IFACES) diff --git a/tests/test_provisioner.py b/tests/test_provisioner.py index ee564d9..798fbad 100644 --- a/tests/test_provisioner.py +++ b/tests/test_provisioner.py @@ -5,7 +5,7 @@ import pytest from arcane_mage.provisioner import TIER_CONFIG, Provisioner, is_api_min_version -from arcane_mage.proxmox import ApiFailure, ApiSuccess +from arcane_mage.proxmox import ApiFailure, ApiSuccess, ProxmoxApi class TestIsApiMinVersion: @@ -108,6 +108,32 @@ async def test_validate_network_missing(self, provisioner: Provisioner, mock_api assert result is False + async def test_validate_network_sdn_vnet(self, provisioner: Provisioner, mock_api: AsyncMock): + mock_api.get_networks.return_value = ApiSuccess( + payload=[ + {"iface": "vmbr0", "type": "bridge"}, + {"iface": "flux50", "type": "vnet"}, + ], + ) + + assert await provisioner.validate_network("node1", "flux50") is True + + async def test_validate_network_listing_fails(self, provisioner: Provisioner, mock_api: AsyncMock): + mock_api.get_networks.return_value = ApiFailure(error="Connection Error") + + assert await provisioner.validate_network("node1", "vmbr0") is False + + async def test_get_networks_requests_any_bridge(self): + api = ProxmoxApi.__new__(ProxmoxApi) + api._do_get = AsyncMock(return_value=ApiSuccess(payload=[])) + + await api.get_networks("node1") + + assert api._do_get.await_count == 1 + call = api._do_get.await_args + assert call is not None + assert call.args[0] == "nodes/node1/network?type=any_bridge" + async def test_validate_iso_version_found(self, provisioner: Provisioner, mock_api: AsyncMock): mock_api.get_storage_content.return_value = ApiSuccess( payload=[