fix(vm_provision_mixin): write Cloud-Init snippet via SSH, not the upload API
Real Proxmox's POST /nodes/{node}/storage/{storage}/upload only accepts
content in {iso, vztmpl, import} — content='snippets' is rejected
outright with a 400 ("does not have a value in the enumeration").
Snippets can only be written directly to the storage's filesystem path.
Found live, right after the previous multipart-upload fix: the VM
shell, disk import, and node-scoped storage selection all succeeded,
then create_vm_from_cloud_init failed with a 400 at the snippet write
step. Resolves the storage's path via the cluster storage config and
writes the file over SSH (base64-piped, to survive arbitrary YAML
content safely).
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import base64
|
||||
import logging
|
||||
import time
|
||||
import yaml
|
||||
@@ -130,6 +130,25 @@ class ProxmoxVMProvisionMixin:
|
||||
"No storage with content='images' found. Configure a storage for VM disks."
|
||||
)
|
||||
|
||||
def _get_storage_path(self, storage: str) -> str:
|
||||
"""Resolve a storage's filesystem path on the node.
|
||||
|
||||
Needed to write Cloud-Init snippets directly: Proxmox's
|
||||
/storage/{s}/upload API only accepts content in {iso, vztmpl,
|
||||
import} — "snippets" is rejected outright, so snippets must be
|
||||
written straight to the filesystem instead. Only dir-backed storages
|
||||
(dir, nfs, cifs, cephfs) expose "path"; those are also the only
|
||||
storage types Proxmox itself allows content='snippets' on.
|
||||
"""
|
||||
config = self._api.storage(storage).get()
|
||||
path = config.get("path")
|
||||
if not path:
|
||||
raise ValueError(
|
||||
f"Storage '{storage}' has no filesystem path (content='snippets' "
|
||||
"requires a dir/nfs/cifs/cephfs-backed storage)"
|
||||
)
|
||||
return path
|
||||
|
||||
def get_image_storages(self) -> List[StorageTargetDict]:
|
||||
"""List node-available storage pools suitable for a new VM's root disk."""
|
||||
targets: List[StorageTargetDict] = []
|
||||
@@ -336,20 +355,19 @@ class ProxmoxVMProvisionMixin:
|
||||
)
|
||||
|
||||
filename = f"{vmid}-user-data.yaml"
|
||||
_logger.debug(f"Uploading Cloud-Init snippet {filename} to {snippet_storage}")
|
||||
_logger.debug(f"Writing Cloud-Init snippet {filename} to {snippet_storage}")
|
||||
|
||||
# Upload to snippet storage. Proxmox's upload endpoint expects the
|
||||
# "filename" parameter to BE the file (multipart), not a name
|
||||
# string with separate content — proxmoxer only builds a
|
||||
# multipart request when the value is an io.IOBase instance,
|
||||
# otherwise it silently sends everything as a plain
|
||||
# form-urlencoded POST, which real Proxmox rejects by dropping
|
||||
# the connection (RemoteDisconnected, no HTTP response at all).
|
||||
file_obj = io.BytesIO(user_data_yaml.encode("utf-8"))
|
||||
file_obj.name = filename
|
||||
self._node_api().storage(snippet_storage).upload.post(
|
||||
content="snippets",
|
||||
filename=file_obj,
|
||||
# Proxmox's /storage/{s}/upload API only accepts content in
|
||||
# {iso, vztmpl, import} — "snippets" is rejected outright
|
||||
# ("does not have a value in the enumeration"). Snippets can only
|
||||
# be written directly to the filesystem, so resolve the storage's
|
||||
# backing path and write the file over SSH instead.
|
||||
storage_path = self._get_storage_path(snippet_storage)
|
||||
encoded = base64.b64encode(user_data_yaml.encode("utf-8")).decode("ascii")
|
||||
self._run_node_command(
|
||||
f"mkdir -p {storage_path}/snippets && "
|
||||
f"echo {encoded} | base64 -d > {storage_path}/snippets/{filename}",
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# Step 7: Configure Cloud-Init references and SSH keys
|
||||
|
||||
Reference in New Issue
Block a user