fix(vm_provision_mixin): query node-scoped storage, not cluster-wide
The cluster-wide /storage endpoint lists every storage regardless of
its "nodes" restriction, so _find_default_image_storage (and the
snippet-storage lookup) could pick a storage not actually available on
the node the VM is being created on. On a real server this stranded a
freshly-created VM shell with no disk attached: "qm importdisk" failed
with "storage 'local-lvm' is not available on node 'pve-02'" after the
VM (VMID 103) already existed. Querying /nodes/{node}/storage instead
fixes this, since Proxmox itself only lists what's available there.
Also adds get_image_storages() and an optional storage= override on
create_vm_from_cloud_init, so callers aren't stuck with auto-detection.
This commit is contained in:
@@ -8,7 +8,12 @@ import yaml
|
||||
from typing import Any, Dict, List
|
||||
from urllib.parse import quote
|
||||
|
||||
from napalm_device_types.models import NetworkTargetDict, VMProvisionResultDict, VMStatusDict
|
||||
from napalm_device_types.models import (
|
||||
NetworkTargetDict,
|
||||
StorageTargetDict,
|
||||
VMProvisionResultDict,
|
||||
VMStatusDict,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,8 +112,16 @@ class ProxmoxVMProvisionMixin:
|
||||
Proxmox's /storage API omits the "enabled" field entirely for storages
|
||||
that were never explicitly toggled — it is not present-and-falsy, it is
|
||||
just absent, defaulting to enabled. Only an explicit 0 means disabled.
|
||||
|
||||
Queries the node-scoped /nodes/{node}/storage endpoint, not the
|
||||
cluster-wide /storage one: a storage can be configured with a "nodes"
|
||||
restriction limiting it to other cluster members, and the cluster-wide
|
||||
list doesn't reflect that — it would happily return a storage this
|
||||
node can't actually see, and "qm importdisk" would fail with
|
||||
"storage 'X' is not available on node 'Y'" after the VM shell was
|
||||
already created.
|
||||
"""
|
||||
for storage in self._api.storage.get():
|
||||
for storage in self._node_api().storage.get():
|
||||
content = storage.get("content", "")
|
||||
if "images" in content and storage.get("enabled", 1) != 0:
|
||||
return storage["storage"]
|
||||
@@ -116,6 +129,27 @@ class ProxmoxVMProvisionMixin:
|
||||
"No storage with content='images' found. Configure a storage for VM disks."
|
||||
)
|
||||
|
||||
def get_image_storages(self) -> List[StorageTargetDict]:
|
||||
"""List node-available storage pools suitable for a new VM's root disk."""
|
||||
targets: List[StorageTargetDict] = []
|
||||
for storage in self._node_api().storage.get():
|
||||
content = storage.get("content", "")
|
||||
if "images" not in content or storage.get("enabled", 1) == 0:
|
||||
continue
|
||||
if storage.get("active", 1) == 0:
|
||||
continue
|
||||
total = storage.get("total") or 0
|
||||
avail = storage.get("avail") or 0
|
||||
targets.append(
|
||||
{
|
||||
"name": storage["storage"],
|
||||
"type": storage.get("type", ""),
|
||||
"total_gb": round(total / (1024**3), 1),
|
||||
"available_gb": round(avail / (1024**3), 1),
|
||||
}
|
||||
)
|
||||
return targets
|
||||
|
||||
def _wait_for_task(self, upid: str, timeout: int = 120) -> None:
|
||||
"""
|
||||
Poll a Proxmox task until completion.
|
||||
@@ -159,6 +193,7 @@ class ProxmoxVMProvisionMixin:
|
||||
image_checksum: str | None = None,
|
||||
ssh_public_keys: List[str] | None = None,
|
||||
disk_resize_gb: int | None = None,
|
||||
storage: str | None = None,
|
||||
download_timeout: int = 300,
|
||||
timeout: int = 180,
|
||||
) -> VMProvisionResultDict:
|
||||
@@ -189,6 +224,8 @@ class ProxmoxVMProvisionMixin:
|
||||
after download (None = no verification)
|
||||
ssh_public_keys: SSH public keys to inject
|
||||
disk_resize_gb: resize root disk to this size (None = no resize)
|
||||
storage: storage pool for the root disk (None = auto-detect first
|
||||
enabled, node-available storage with content='images')
|
||||
download_timeout: max seconds for the image download (skipped if cached)
|
||||
timeout: max seconds for the remaining provisioning steps
|
||||
|
||||
@@ -222,7 +259,7 @@ class ProxmoxVMProvisionMixin:
|
||||
local_path = self._download_cloud_image(
|
||||
image_url, image_checksum, timeout=download_timeout
|
||||
)
|
||||
image_storage = self._find_default_image_storage()
|
||||
image_storage = storage or self._find_default_image_storage()
|
||||
|
||||
_logger.info(f"Importing {local_path} into VM {vmid} on storage {image_storage}")
|
||||
self._run_node_command(
|
||||
@@ -271,9 +308,10 @@ class ProxmoxVMProvisionMixin:
|
||||
self._node_api().qemu(vmid).config.post(**config_args)
|
||||
|
||||
# Step 5: Verify snippet storage exists
|
||||
# (enabled is absent-not-falsy on Proxmox — see _find_default_image_storage)
|
||||
# (enabled is absent-not-falsy, and node-scoping matters — see
|
||||
# _find_default_image_storage)
|
||||
_logger.info("Checking for snippet storage...")
|
||||
storages = self._api.storage.get()
|
||||
storages = self._node_api().storage.get()
|
||||
snippet_storage = None
|
||||
for storage in storages:
|
||||
content = storage.get("content", "")
|
||||
|
||||
Reference in New Issue
Block a user