-
Notifications
You must be signed in to change notification settings - Fork 88
feat(vm-manager): add Cloud-Init support for Unraid VMs #2536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Implement Cloud-Init configuration UI and backend for automatic VM provisioning on first boot. This enables zero-touch deployment of Linux VMs using cloud images. Features: - Add Cloud-Init section in VM Advanced View with enable toggle - Implement Basic mode with form-based configuration: - Identity: hostname, default user, password, SSH keys, root login - System: timezone - Packages: update/upgrade toggle, package installation list - Commands: arbitrary shell commands on boot - Implement Advanced mode for raw user-data and network-config YAML - Persist all settings to cloud-init.json alongside VM files - Generate VFAT disk image (cloud-init.img) with cidata label - Attach Cloud-Init disk as VirtIO device for optimal performance - Support device type override in libvirt XML generation Technical changes: - Custom.form.php: UI components and save/load logic for create/update - libvirt_helpers.php: create_cloud_init_iso() with VFAT generation - libvirt.php: Added device attribute passthrough for disk elements (required) Tested with: - Ubuntu 24.04.2 LTS Cloud Image (noble-server-cloudimg-amd64.img) Source: https://cloud-images.ubuntu.com/noble/current/ Date: 2026-01-08 - Verified: hostname, timezone, user creation, password auth, ssh key, package installation, and runcmd execution all applied correctly - Confirmed VFAT cidata label detected by Cloud-Init NoCloud datasource
WalkthroughAdds Cloud‑Init support (user-data/network-config generation and ISO creation), integrates Cloud‑Init UI and persistence into VM create/edit flows, records per-disk Changes
Sequence DiagramsequenceDiagram
participant User
participant Form as VM Form Handler
participant CloudInit as Cloud‑Init Generator
participant FS as File System
participant Libvirt as VM Manager
User->>Form: Submit VM form (cloudinit enabled)
Form->>Form: Validate inputs / select mode
Form->>CloudInit: generate user-data & network-config
CloudInit->>FS: create_cloud_init_iso (dd, format, mount, write files, unmount)
CloudInit-->>Form: return ISO path or error
Form->>FS: write cloud-init.json / cloud-init.user-data / network-config
Form->>Libvirt: attach cloud-init ISO as disk (include deviceType)
Libvirt->>FS: persist updated VM config
Form->>User: Report creation/update result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php`:
- Around line 53-58: The three file write calls using
file_put_contents($strMountPoint . '/user-data', $strUserData),
file_put_contents($strMountPoint . '/network-config', $strNetworkConfig) and
file_put_contents($strMountPoint . '/meta-data', ...) need explicit error
handling: check each file_put_contents return value for === false and handle
failures (log an error via existing logger or trigger an exception/return an
error code) so a failed write (permission/disk/full) is reported and the caller
can abort/clean up rather than silently producing a bad cloud-init image;
include the target path ($strMountPoint . '/...') in the error message to aid
debugging.
- Around line 62-70: The rmdir($strMountPoint) call is occurring before checking
the umount result, which can remove the mountpoint even when umount failed;
change the order so you first run exec("umount " .
escapeshellarg($strMountPoint) ..., check if $return_var !== 0 and on failure
log the error (using error_log("Cloud-Init image unmount failed: " .
implode("\n", $output))) and return false, and only after a successful umount
call rmdir($strMountPoint); optionally log or handle rmdir failures after the
successful unmount.
In `@emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php`:
- Around line 388-499: Extract the duplicated cloud-init handling into a
reusable helper in libvirt_helpers.php (e.g., function
process_cloud_init_config($strVMPath, $cloudinitPost)) that ensures the VM path
exists, builds and writes cloud-init.json, generates user-data either by calling
generate_cloud_init_userdata($cloudinitPost) for 'basic' mode or using provided
userdata, writes cloud-init.user-data and cloud-init.network-config, and invokes
create_cloud_init_iso($strVMPath, $strUserData, $strNetworkConfig); replace the
duplicated blocks in both the creation flow and the update flow with calls to
process_cloud_init_config($_POST domain path, $_POST['cloudinit']) so all logic
is centralized and retained.
- Around line 178-181: The directory creation uses overly permissive mode 0777
for $strVMPath; change the mkdir call in Custom.form.php to create the VM
directory with restrictive permissions (e.g., 0700 or 0755) and ensure ownership
is set to the appropriate user (use chown/chgrp after mkdir or pass correct
uid/gid where applicable); keep the existing is_dir check and recursive flag but
replace 0777 with the chosen restrictive mode and add a safe chown to the
daemon/user that should own VM data.
- Around line 205-272: The code concatenates user-controlled values into
$strUserData inside the cloudInitMode == 'basic' branch (variables: $hostname,
$timezone, $user, $pass, $keys, $packages, $runcmd, arrays
$arrPkg/$arrKeys/$arrCmd), which creates a YAML injection risk; stop building
YAML by string concatenation and instead construct a PHP associative
array/structure representing the cloud-config (users, packages, runcmd, etc.)
and serialize it with a proper YAML library (e.g., yaml_emit or
Symfony\Component\Yaml\Yaml::dump) so all values are correctly escaped/quoted,
or if a library is unavailable, ensure every scalar and multiline value is
safely escaped/quoted (use literal block style for multiline fields like runcmd
and parse $keys/$packages into arrays) before writing to $strUserData.
🧹 Nitpick comments (2)
emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php (1)
46-51: Consider resetting$outputarray before eachexeccall.The
$outputarray accumulates results from previous commands (format, mount, umount). If mount fails, the error log at line 49 may include output from the format command, making debugging harder.Proposed fix
// Mount image + $output = []; exec("mount -o loop " . escapeshellarg($strImgPath) . " " . escapeshellarg($strMountPoint) . " 2>&1", $output, $return_var);Similarly before the umount call:
// Sync and Unmount exec("sync"); + $output = []; exec("umount " . escapeshellarg($strMountPoint) . " 2>&1", $output, $return_var);emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php (1)
563-575: Remove commented-out debug logging.Debug logging code is commented out but left in the codebase. This adds noise and could accidentally be enabled in production.
🧹 Suggested fix
- // Debug logging to specific file - // file_put_contents("/var/log/cloudinit_debug.log", "Loading config for $strVMName from $strVMPath\n", FILE_APPEND); - // Load JSON config first for UI fields if (file_exists($strVMPath . '/cloud-init.json')) { $json_content = file_get_contents($strVMPath . '/cloud-init.json'); $arrCloudInitConfig = json_decode($json_content, true); if ($arrCloudInitConfig) { $arrConfig['cloudinit'] = array_merge($arrConfig['cloudinit'], $arrCloudInitConfig); - // file_put_contents("/var/log/cloudinit_debug.log", "Loaded JSON: " . print_r($arrCloudInitConfig, true) . "\n", FILE_APPEND); - } else { - // file_put_contents("/var/log/cloudinit_debug.log", "Failed to decode JSON: $json_content\n", FILE_APPEND); }
Exclude xmldesc textarea from form view POST data serialization
Refactored the duplicate Cloud-Init YAML generation logic into a reusable helper function 'generate_cloud_init_userdata'. This updates both the VM creation and update paths to use the shared function. Addresses CodeRabbit feedback: unraid#2536 (comment)
Replaced manual string concatenation in Cloud-Init YAML generation with structured array construction and a safe YAML encoder 'my_yaml_encode'. This ensures user input is properly quoted and escaped, preventing potential YAML injection attacks where new keys could be injected via newlines. Addresses CodeRabbit feedback: unraid#2536 (comment)
Implemented feedback from CodeRabbit to improve robustness and security: - Added error handling for Cloud-Init file writes in 'libvirt_helpers.php'. - Corrected race condition ensuring `rmdir` only runs after successful `umount`. - Deepened security by restricting new VM directory permissions to `0755`. - Added cleanup of `cloud-init.*` files in 'domain_delete'. Addresses following CodeRabbit feedbacks: - unraid#2536 (comment) - unraid#2536 (comment) - unraid#2536 (comment)
Removed temporary file_put_contents debug logging calls and associated dead code (empty else block) from Custom.form.php that were used during development of the Cloud-Init refactoring.
Resets the $output array before exec calls in create_cloud_init_iso to prevent output accumulation. This ensures that error logs for a specific command (like mount) do not contain unrelated output from previous commands (like formatting).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
emhttp/plugins/dynamix.vm.manager/include/libvirt.php (1)
1778-1801: Guard$pathinfo/$diskwhen falling back to default VM directories.When the primary disk path is missing,
$pathinfoand$diskare undefined but still used to build paths andunlink(). This raises notices and can target"$dir/.cfg"/"$dir/.xml"accidentally.🔧 Suggested fix (safe defaults + conditional deletes)
- $dir = ''; + $dir = ''; + $disk = null; + $pathinfo = null; if (!empty($disks) && array_key_exists('file', $disks[0])) { $disk = $disks[0]['file']; $pathinfo = pathinfo($disk); $dir = $pathinfo['dirname']; } elseif (is_dir("/mnt/user/domains/$domain")) { $dir = "/mnt/user/domains/$domain"; } elseif (is_dir("/mnt/cache/domains/$domain")) { $dir = "/mnt/cache/domains/$domain"; } if ($dir) { // remove the vm config $cfg_vm = $dir.'/'.$domain.'.cfg'; if (is_file($dir.'/cloud-init.img')) unlink($dir.'/cloud-init.img'); if (is_file($dir.'/cloud-init.json')) unlink($dir.'/cloud-init.json'); if (is_file($dir.'/cloud-init.user-data')) unlink($dir.'/cloud-init.user-data'); if (is_file($dir.'/cloud-init.network-config')) unlink($dir.'/cloud-init.network-config'); if (is_file($cfg_vm)) unlink($cfg_vm); - $cfg = $dir.'/'.$pathinfo['filename'].'.cfg'; - $xml = $dir.'/'.$pathinfo['filename'].'.xml'; - if (is_file($disk)) unlink($disk); - if (is_file($cfg)) unlink($cfg); - if (is_file($xml)) unlink($xml); + if ($pathinfo) { + $cfg = $dir.'/'.$pathinfo['filename'].'.cfg'; + $xml = $dir.'/'.$pathinfo['filename'].'.xml'; + if (is_file($cfg)) unlink($cfg); + if (is_file($xml)) unlink($xml); + } + if ($disk && is_file($disk)) unlink($disk);
🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix.vm.manager/include/libvirt.php`:
- Around line 619-621: Validate and whitelist the disk device type before
emitting it into the libvirt XML: replace the direct use of $disk['deviceType']
when assigning $strDevice (used in the $diskstr XML assembly) with a whitelist
check that only allows "disk" or "lun" and falls back to "disk" for any
other/empty value; perform this validation where $strDevice is set so the
subsequent "$diskstr .= ... device='".$strDevice."'" construction can only ever
include an allowed value.
In `@emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php`:
- Around line 174-227: The cloud-init ISO creation currently continues silently
on failure; after calling create_cloud_init_iso($strVMPath, $strUserData,
$strNetworkConfig) check the returned $isoPath and if it is false/empty, stop
further processing and surface an error (e.g., set an error message and abort
the form submission/VM creation flow) instead of proceeding to add the disk;
update the block that currently does "if ($isoPath) { ...
$_POST['disk'][$newDiskIndex] = ..." to handle the failure case by
returning/throwing an error or setting a visible failure state so callers of
create_cloud_init_iso, the cloud-init.json write and the VM creation are not
performed when $isoPath is invalid.
- Around line 323-367: Capture the ISO path returned by create_cloud_init_iso by
changing the call to assign its return to $isoPath
(create_cloud_init_iso($strVMPath, $strUserData, $strNetworkConfig) → $isoPath =
create_cloud_init_iso(...)), then if $isoPath is non-empty append a new disk
entry to $_POST['disk'] (use count($_POST['disk'] ?? []) for index) with keys
'source' => $isoPath, 'bus' => 'sata', 'type' => 'cdrom' so the cloud-init ISO
is attached during the update path; relevant symbols: create_cloud_init_iso,
$isoPath, $_POST['disk'], $strVMPath, $strUserData, $strNetworkConfig.
- Abort VM creation if cloud-init ISO generation fails, preventing invalid VM states. - Capture generated ISO path during VM updates and automatically attach it as a SATA CD-ROM device. Addresses CodeRabbit feedback: unraid#2536 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php`:
- Around line 222-229: The new disk added to $_POST['disk'] uses keys that don't
match the update path and what config_to_xml() expects; change the disk entry
created in the add-Cloud-Init block so it uses the same schema as
config_to_xml(): include 'deviceType' and 'image' (not 'source'/'type'), set
'driver' and 'bus' consistently (e.g., 'driver'=>'raw', 'bus'=>'virtio') so
$_POST['disk'][$newDiskIndex] uses the same keys as other code paths and
config_to_xml() can process it.
- The recent commit 8ad110d incorrectly used 'source' and 'type' keys for the Cloud-Init ISO disk attachment in the update path. - This change standardizes the schema to use 'deviceType', 'image', 'driver', and 'bus' keys, allowing config_to_xml to correctly process the disk. Fixes regression introduced in: unraid@8ad110d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php`:
- Around line 368-378: The update path currently calls
create_cloud_init_iso($strVMPath, $strUserData, $strNetworkConfig) and silently
continues if it returns false, causing cloud-init to be omitted; change this to
mirror the create path by checking $isoPath and, on failure, log/emit an error
and halt the update (e.g., return/exit with an error message) so the user is
notified and the VM update does not proceed without the cloud-init disk; update
the block around create_cloud_init_iso and the subsequent $_POST['disk']
population to perform this error check and early exit.
- Ensure VM update is aborted if Cloud-Init ISO creation fails. - This consistency prevents silent failures where the VM would be updated without the intended Cloud-Init configuration. Addresses CodeRabbit feedback: unraid#2536 (comment)
…andling - Fixes a bug where Cloud-Init disks were duplicated on VM updates by implementing smarter disk replacement logic. - Adds support for custom ISO paths with automatic directory handling and validation. - Implements security hardening to restrict paths to /mnt/ and prevent injection. - improving error handling to strictly abort operations on ISO creation failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php`:
- Around line 217-229: The current start-with '/mnt/' check can be bypassed via
path traversal; update the validation around $customISOPath (the same checks
that use strpos and preg_match) to explicitly reject any '..' path segments
before proceeding (e.g., detect sequences like '/..' or '/./..' via a regex such
as checking for '(^|/)\\.\\.(/|$)' or a simple strpos for '..'), and apply this
hardening in both the create and update blocks where $customISOPath is validated
so paths containing '..' are rejected early.
🧹 Nitpick comments (1)
emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php (1)
200-202: Remove the duplicateuserdataentry in the Cloud‑Init config array.
Duplicate keys are silently overwritten and add confusion for future edits.🧹 Suggested cleanup (apply in both blocks)
- 'userdata' => $_POST['cloudinit']['userdata'] ?? '', - 'userdata' => $_POST['cloudinit']['userdata'] ?? '', + 'userdata' => $_POST['cloudinit']['userdata'] ?? '',Also applies to: 367-368
- Reject paths containing '..' segments to prevent directory traversal outside /mnt/. - This ensures that the 'starts with /mnt/' check cannot be bypassed. Addresses CodeRabbit feedback: unraid#2536 (comment)
Details:
Implement Cloud-Init configuration UI and backend for automatic VM provisioning on first boot. This enables zero-touch deployment of Linux VMs using cloud images.
Previews:
Features:
Technical changes:
Tested with:
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.