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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
3 changes: 2 additions & 1 deletion src/arcane_mage/provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/arcane_mage/proxmox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
28 changes: 27 additions & 1 deletion tests/test_provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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=[
Expand Down