_(Libvirt volume info)_
_(btrfs filesystem show)_:
diff --git a/emhttp/plugins/dynamix.vm.manager/include/fs_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/fs_helpers.php
new file mode 100644
index 0000000000..3825cedd4d
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/include/fs_helpers.php
@@ -0,0 +1,99 @@
+ $src,
+ 'dst' => $dst,
+ 'would_copy' => false,
+ 'copied' => false,
+ 'error' => null
+ ];
+
+ if (!file_exists($src)) {
+ $result['error'] = 'source not found';
+ return $result;
+ }
+
+ $dst_dir = dirname($dst);
+ if (!is_dir($dst_dir)) {
+ if ($dry_run) {
+ $result['would_copy'] = true;
+ return $result;
+ }
+ if (!@mkdir($dst_dir, 0755, true)) {
+ $result['error'] = 'failed to create dest dir';
+ return $result;
+ }
+ }
+
+ if (file_exists($dst)) {
+ if (files_identical($src, $dst)) {
+ return $result; // identical, nothing to do
+ }
+ $result['would_copy'] = true;
+ } else {
+ $result['would_copy'] = true;
+ }
+
+ if ($dry_run) return $result;
+
+ if (@copy($src, $dst)) {
+ $result['copied'] = true;
+ } else {
+ $result['error'] = 'copy_failed';
+ }
+
+ return $result;
+}
+
+function dir_copy($src, $dst) {
+ if (!is_dir($src)) return false;
+ if (!is_dir($dst)) {
+ if (!@mkdir($dst, 0755, true)) return false;
+ }
+ $items = scandir($src);
+ foreach ($items as $item) {
+ if ($item === '.' || $item === '..') continue;
+ $s = $src . DIRECTORY_SEPARATOR . $item;
+ $d = $dst . DIRECTORY_SEPARATOR . $item;
+ if (is_dir($s)) {
+ if (!dir_copy($s, $d)) return false;
+ } else {
+ if (file_exists($d)) {
+ if (files_identical($s, $d)) continue;
+ }
+ if (!@copy($s, $d)) return false;
+ }
+ }
+ return true;
+}
+
+function dir_remove($dir) {
+ if (!is_dir($dir)) return false;
+ $items = scandir($dir);
+ foreach ($items as $item) {
+ if ($item === '.' || $item === '..') continue;
+ $path = $dir . DIRECTORY_SEPARATOR . $item;
+ if (is_dir($path)) {
+ dir_remove($path);
+ } else {
+ @unlink($path);
+ }
+ }
+ return @rmdir($dir);
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
index 4b98d16f0c..a9643d538b 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
@@ -5,6 +5,64 @@
# run & log functions
. /etc/rc.d/rc.runlog
+
+# Sync domain data if IMAGE_FILE and OLD_IMAGE_FILE differ
+DOMAIN_CFG=/boot/config/domain.cfg
+
+# Read values from domain.cfg
+eval $(grep -E '^(IMAGE_FILE|OLD_IMAGE_FILE)=' "$DOMAIN_CFG")
+
+# Remove quotes
+IMAGE_FILE="${IMAGE_FILE%\"}"
+IMAGE_FILE="${IMAGE_FILE#\"}"
+OLD_IMAGE_FILE="${OLD_IMAGE_FILE%\"}"
+OLD_IMAGE_FILE="${OLD_IMAGE_FILE#\"}"
+
+# Proceed only if both variables are set and OLD_IMAGE_FILE exists
+if [ -n "$IMAGE_FILE" ] && [ -n "$OLD_IMAGE_FILE" ] && [ "$IMAGE_FILE" != "$OLD_IMAGE_FILE" ]; then
+ if [ ! -e "$OLD_IMAGE_FILE" ]; then
+ log "OLD_IMAGE_FILE not found: $OLD_IMAGE_FILE — skipping sync"
+ else
+ log "IMAGE_FILE and OLD_IMAGE_FILE differ, syncing..."
+
+ TMP_MNT=/etc/libvirt-sync
+ IMG_FILE_NAME=$(basename "$IMAGE_FILE")
+ OLD_IMG_FILE_NAME=$(basename "$OLD_IMAGE_FILE")
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+
+ if [[ "$OLD_IMAGE_FILE" == *.img ]]; then
+ # Backup image before mounting
+ BACKUP_PATH="${OLD_IMAGE_FILE%.img}.bak-${TIMESTAMP}.img"
+ log "Creating backup of OLD_IMAGE_FILE: $BACKUP_PATH"
+ cp -p "$OLD_IMAGE_FILE" "$BACKUP_PATH"
+
+ log "Mounting $OLD_IMAGE_FILE to $TMP_MNT"
+ mkdir -p "$TMP_MNT"
+ mount "$OLD_IMAGE_FILE" "$TMP_MNT"
+ log "Copying full contents from image to directory $IMAGE_FILE"
+ rsync -a --exclude="$OLD_IMG_FILE_NAME" "$TMP_MNT/" "$IMAGE_FILE/"
+ umount "$TMP_MNT"
+ elif [[ "$IMAGE_FILE" == *.img ]]; then
+ log "Mounting $IMAGE_FILE to $TMP_MNT"
+ mkdir -p "$TMP_MNT"
+ mount "$IMAGE_FILE" "$TMP_MNT"
+ log "Copying full contents from directory $OLD_IMAGE_FILE to image"
+ rsync -a --exclude="$IMG_FILE_NAME" --exclude='*.bak-*.img' "$OLD_IMAGE_FILE/" "$TMP_MNT/"
+ umount "$TMP_MNT"
+ else
+ log "Both IMAGE_FILE and OLD_IMAGE_FILE are directories, copying full contents"
+ rsync -a --exclude="$IMG_FILE_NAME" "$OLD_IMAGE_FILE/" "$IMAGE_FILE/"
+ fi
+
+ # Update OLD_IMAGE_FILE in domain.cfg
+ log "Updating OLD_IMAGE_FILE in $DOMAIN_CFG"
+ sed -i "s|^OLD_IMAGE_FILE=.*|OLD_IMAGE_FILE=\"$IMAGE_FILE\"|" "$DOMAIN_CFG"
+ fi
+else
+ log "IMAGE_FILE and OLD_IMAGE_FILE match, or one is unset — skipping sync"
+fi
+
+
# missing qemu directory would indicate new libvirt image file created
if [ ! -d /etc/libvirt/qemu ]; then
log "initializing /etc/libvirt"
@@ -36,3 +94,7 @@ if [ -s /var/log/vfio-pci-errors ]; then
echo "vfio-pci bind error" > /run/libvirt/qemu/autostarted
/usr/local/emhttp/webGui/scripts/notify -e "VM Autostart disabled" -s "vfio-pci-errors " -d "VM Autostart disabled due to vfio-bind error" -m "Please review /var/log/vfio-pci-errors" -i "alert" -l "/VMs"
fi
+
+# Copy XML from VM Directories to QEMU directory/
+/usr/local/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
+#
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig
index 12ae126341..5d6e0f269e 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig
@@ -15,7 +15,8 @@
$cfgfile = "/boot/config/domain.cfg";
$cfg_defaults = [
"SERVICE" => "disable",
- "IMAGE_FILE" => "/mnt/user/system/libvirt/libvirt.img",
+ "IMAGE_FILE" => "/mnt/user/system/libvirt/",
+ "OLD_IMAGE_FILE" => "/mnt/user/system/libvirt/",
"IMAGE_SIZE" => "1",
"DEBUG" => "no",
"DOMAINDIR" => "/mnt/user/domains/",
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
new file mode 100755
index 0000000000..19936df874
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
@@ -0,0 +1,167 @@
+#!/usr/bin/php
+
+
+
+/* ---------------------------------------------------------
+ * Standard includes
+ * --------------------------------------------------------- */
+$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
+require_once "$docroot/webGui/include/Helpers.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/fs_helpers.php";
+
+/* ---------------------------------------------------------
+ * Read default VM directory from Unraid domain.cfg
+ * --------------------------------------------------------- */
+function get_default_domain_dir() {
+ $cfg = '/boot/config/domain.cfg';
+
+ if (!file_exists($cfg)) {
+ return null;
+ }
+
+ $lines = file($cfg, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ if ($lines === false) {
+ return null;
+ }
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+
+ if ($line === '' || $line[0] === '#') {
+ continue;
+ }
+
+ if (preg_match('/^DOMAINDIR="([^"]+)"/', $line, $m)) {
+ return rtrim($m[1], '/');
+ }
+ }
+
+ return null;
+}
+
+/* ---------------------------------------------------------
+ * Connect to libvirt
+ * --------------------------------------------------------- */
+$lv = libvirt_connect('qemu:///system', false);
+if (!$lv) {
+ die("Failed to connect to libvirt\n");
+}
+
+/* Running VMs (or all, if you prefer libvirt_list_all_domains) */
+$domains = libvirt_list_domains($lv);
+if ($domains === false) {
+ die("Failed to list domains\n");
+}
+
+$default_domain_dir = get_default_domain_dir();
+
+$vms = [];
+
+/* ---------------------------------------------------------
+ * Enumerate VMs
+ * --------------------------------------------------------- */
+foreach ($domains as $dom) {
+
+ $domget = libvirt_domain_lookup_by_name($lv, $dom);
+ if ($domget === false) {
+ continue;
+ }
+
+ $xml = libvirt_domain_get_xml_desc($domget, 0);
+ if ($xml === false) {
+ continue;
+ }
+
+ $sx = new SimpleXMLElement($xml);
+
+ $vm_name = (string)$sx->name;
+ $uuid = (string)$sx->uuid;
+
+ /* -----------------------------------------------------
+ * Read storage metadata (Unraid vmtemplate)
+ * ----------------------------------------------------- */
+ $metadata_storage = null;
+
+ if (isset($sx->metadata)) {
+ foreach ($sx->metadata->children() as $child) {
+ if ($child->getName() === 'vmtemplate') {
+ $metadata_storage = trim((string)$child['storage']);
+ break;
+ }
+ }
+ }
+
+ /* -----------------------------------------------------
+ * Resolve filesystem path
+ * Treat empty, null, or "default" as DOMAINDIR
+ * ----------------------------------------------------- */
+ if ($metadata_storage === null || $metadata_storage === '' || strtolower($metadata_storage) === 'default') {
+ /* TRUE default storage */
+ $path_root = $default_domain_dir; // e.g. /mnt/user/domains2
+ $storage_name = 'default';
+ } else {
+ /* Explicit Unraid pool */
+ $path_root = str_replace("/mnt/user/","/mnt/$metadata_storage/",$default_domain_dir);
+ $storage_name = $metadata_storage;
+ }
+
+
+ $path = $path_root
+ ? $path_root . '/' . $vm_name
+ : null;
+
+
+ /* Filesystem existence check (remove quotes for is_dir) */
+ $exists = ($path_root && is_dir($path_root . '/' . $vm_name));
+
+ /* -----------------------------------------------------
+ * Store result
+ * ----------------------------------------------------- */
+ $vms[$vm_name] = [
+ 'uuid' => $uuid,
+ 'storage' => $storage_name,
+ 'path' => $path,
+ 'path_shell' => $path ? escapeshellarg($path) : null,
+ 'exists' => $exists,
+ ];
+}
+
+/* ---------------------------------------------------------
+ * Output
+ * --------------------------------------------------------- */
+#print_r($vms);
+ksort($vms,SORT_NATURAL);
+file_put_contents("/boot/config/plugins/dynamix.vm.manager/vms.json",json_encode($vms,JSON_PRETTY_PRINT));
+
+file_put_contents("/tmp/Stopcopy","");
+foreach ($vms as $vm => $vmdetail) {
+
+ $from_file = "/etc/libvirt/qemu/$vm.xml";
+ $to_file = $vmdetail['path']."/$vm.xml";
+ #echo " from:$from_file to:$to_file\n";
+ if ($vmdetail['exists']) {
+ $res = copy_if_different($from_file, $to_file, false);
+ $msg = "$vm from:$from_file to:$to_file";
+ if (!empty($res['error'])) {
+ $msg .= " ERROR:" . $res['error'];
+ } elseif (!empty($res['copied'])) {
+ $msg .= " COPIED";
+ } elseif (!empty($res['would_copy'])) {
+ $msg .= " WOULD_COPY";
+ } else {
+ $msg .= " SKIPPED_IDENTICAL";
+ }
+ file_put_contents("/tmp/Stopcopy", $msg . "\n", FILE_APPEND);
+ } else file_put_contents("/tmp/Stopcopy","Nocpy $vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_file";
+}
+?>
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtmigrate b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtmigrate
new file mode 100644
index 0000000000..c10d93a618
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtmigrate
@@ -0,0 +1,498 @@
+#!/usr/bin/php
+
+
+
+$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
+require_once "$docroot/webGui/include/Helpers.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/fs_helpers.php";
+$libvirt_location ="/etc/libvirtold";
+
+/* ---------------------------------------------------------
+ * Load vms.json created by libvirtcopy
+ * --------------------------------------------------------- */
+function load_vms_json() {
+ $vms_json_path = '/boot/config/plugins/dynamix.vm.manager/vms.json';
+ if (!file_exists($vms_json_path)) {
+ return [];
+ }
+
+ $json = @json_decode(file_get_contents($vms_json_path), true);
+ return is_array($json) ? $json : [];
+}
+
+/* filesystem helpers moved to include/fs_helpers.php */
+
+/* ---------------------------------------------------------
+ * Migrate NVRAM file to VM folder and update XML
+ * --------------------------------------------------------- */
+function migrate_nvram_file($valid_nvram, $vm_path, $vm_uuid, $vm_name, $dry_run = false) {
+ global $libvirt_location;
+ // Create nvram subdirectory
+ $nvram_dest_dir = $vm_path . '/nvram';
+ if (!is_dir($nvram_dest_dir)) {
+ if (!$dry_run && !@mkdir($nvram_dest_dir, 0755, true)) {
+ return [
+ 'success' => false,
+ 'error' => 'Failed to create nvram directory: ' . $nvram_dest_dir
+ ];
+ }
+ }
+
+ // Determine destination filename (preserve original)
+ $src_file = $valid_nvram['file'];
+ $dest_file = $nvram_dest_dir . '/' . basename($src_file);
+
+ // Copy NVRAM file (compare first)
+ $would_copy = false;
+ $copied = false;
+ if (file_exists($dest_file)) {
+ $same = false;
+ if (filesize($src_file) === filesize($dest_file)) {
+ $hs = @md5_file($src_file);
+ $hd = @md5_file($dest_file);
+ if ($hs !== false && $hd !== false && $hs === $hd) {
+ $same = true;
+ }
+ }
+ if (!$same) $would_copy = true;
+ } else {
+ $would_copy = true;
+ }
+
+ if ($would_copy) {
+ if ($dry_run) {
+ // indicate would-copy in result, don't actually copy
+ } else {
+ if (!@copy($src_file, $dest_file)) {
+ return [
+ 'success' => false,
+ 'error' => "Failed to copy NVRAM file from $src_file to $dest_file"
+ ];
+ }
+ $copied = true;
+ }
+ }
+
+ // Update XML file
+ $xml_old_path = $libvirt_location . "/qemu/$vm_name.xml";
+ $xml_new_path = "$vm_path/$vm_name.xml";
+
+ // Read old XML
+ if (!file_exists($xml_old_path)) {
+ @unlink($dest_file); // Rollback
+ return [
+ 'success' => false,
+ 'error' => "XML file not found: $xml_old_path"
+ ];
+ }
+
+ $xml_content = file_get_contents($xml_old_path);
+ $xml = @simplexml_load_string($xml_content);
+
+ if ($xml === false) {
+ if (!$dry_run) @unlink($dest_file); // Rollback
+ return [
+ 'success' => false,
+ 'error' => "Failed to parse XML: $xml_old_path"
+ ];
+ }
+
+ // Update nvram path in XML
+ if (isset($xml->os->nvram)) {
+ $xml->os->nvram = $dest_file;
+ }
+
+ // Write updated XML to new location
+ $xml_formatted = $xml->asXML();
+ if (!$dry_run && !@file_put_contents($xml_new_path, $xml_formatted)) {
+ @unlink($dest_file); // Rollback
+ return [
+ 'success' => false,
+ 'error' => "Failed to write updated XML to: $xml_new_path"
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'nvram_src' => $src_file,
+ 'nvram_dest' => $dest_file,
+ 'xml_old_path' => $xml_old_path,
+ 'xml_new_path' => $xml_new_path,
+ 'dry_run' => $dry_run,
+ 'would_copy' => $would_copy,
+ 'copied' => $copied
+ ];
+}
+
+/* ---------------------------------------------------------
+ * Perform NVRAM migration for valid files
+ * --------------------------------------------------------- */
+function perform_migration($valid_nvrams, $dry_run = false) {
+ if (empty($valid_nvrams)) {
+ return ['migrated' => 0, 'failed' => 0, 'errors' => []];
+ }
+
+ $vms_json = load_vms_json();
+ if (empty($vms_json)) {
+ return [
+ 'migrated' => 0,
+ 'failed' => count($valid_nvrams),
+ 'errors' => [['error' => 'vms.json not found or empty']]
+ ];
+ }
+
+ $migrated = 0;
+ $failed = 0;
+ $results = [];
+ global $libvirt_location;
+ $snapshot_moves = [];
+ $moved_snapshotdb = [];
+
+ foreach ($valid_nvrams as $nvram_item) {
+ $vm_name = $nvram_item['vm_name'];
+ $vm_uuid = $nvram_item['uuid'];
+
+ // Find VM in vms.json
+ if (!isset($vms_json[$vm_name])) {
+ $failed++;
+ $results[] = [
+ 'vm_name' => $vm_name,
+ 'success' => false,
+ 'error' => "VM not found in vms.json"
+ ];
+ continue;
+ }
+
+ $vm_path = $vms_json[$vm_name]['path'];
+ if (empty($vm_path)) {
+ $failed++;
+ $results[] = [
+ 'vm_name' => $vm_name,
+ 'success' => false,
+ 'error' => "VM path not found in vms.json"
+ ];
+ continue;
+ }
+
+ // Ensure snapshotdb for this VM is moved once
+ if (!isset($moved_snapshotdb[$vm_name])) {
+ $moved_snapshotdb[$vm_name] = true;
+ $old_snap_dir = $libvirt_location . "/qemu/snapshotdb/" . $vm_name;
+ $new_snap_dir = rtrim($vm_path, '/') . "/snapshotdb";
+
+ // Only move snapshotdb if snapshots.db exists and is non-empty
+ $snap_db_file = $old_snap_dir . '/snapshots.db';
+ $snap_contents = [];
+ if (file_exists($snap_db_file) && filesize($snap_db_file) > 0) {
+ $snap_contents = load_snapshot_db($vm_name);
+ }
+
+ if (!empty($snap_contents)) {
+ if ($dry_run) {
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'would_move' => true,
+ 'dry_run' => true
+ ];
+ } else {
+ // If destination exists, merge; otherwise attempt rename then fallback to copy
+ if (is_dir($new_snap_dir)) {
+ $ok = dir_copy($old_snap_dir, $new_snap_dir);
+ if ($ok) {
+ $removed = dir_remove($old_snap_dir);
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'success' => $ok && $removed,
+ 'action' => 'merge'
+ ];
+ } else {
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'success' => false,
+ 'error' => 'Failed to merge snapshotdb into existing destination'
+ ];
+ }
+ } else {
+ if (@rename($old_snap_dir, $new_snap_dir)) {
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'success' => true,
+ 'action' => 'rename'
+ ];
+ } else {
+ // Fallback to copy
+ $ok = dir_copy($old_snap_dir, $new_snap_dir);
+ if ($ok) {
+ $removed = dir_remove($old_snap_dir);
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'success' => $ok && $removed,
+ 'action' => 'copy'
+ ];
+ } else {
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'src' => $old_snap_dir,
+ 'dest' => $new_snap_dir,
+ 'success' => false,
+ 'error' => 'Failed to move or copy snapshotdb'
+ ];
+ }
+ }
+ }
+ }
+ } else {
+ // No snapshots present; skip moving/creating snapshotdb
+ $snapshot_moves[] = [
+ 'vm_name' => $vm_name,
+ 'found' => false,
+ 'reason' => 'snapshots.db missing or empty'
+ ];
+ }
+ }
+
+ // Perform migration
+ $migration_result = migrate_nvram_file($nvram_item, $vm_path, $vm_uuid, $vm_name, $dry_run);
+ $migration_result['vm_name'] = $vm_name;
+
+ if ($migration_result['success']) {
+ $migrated++;
+ } else {
+ $failed++;
+ }
+
+ $results[] = $migration_result;
+ }
+
+ return [
+ 'migrated' => $migrated,
+ 'failed' => $failed,
+ 'results' => $results,
+ 'snapshotdb_moves' => $snapshot_moves
+ ];
+}
+
+/* ---------------------------------------------------------
+ * Load snapshot database for a VM
+ * --------------------------------------------------------- */
+function load_snapshot_db($vm_name) {
+ global $libvirt_location;
+ $snap_db = $libvirt_location . "/qemu/snapshotdb/" . $vm_name . "/snapshots.db";
+ if (!file_exists($snap_db)) {
+ return [];
+ }
+
+ $json = @json_decode(file_get_contents($snap_db), true);
+ return is_array($json) ? $json : [];
+}
+
+/* ---------------------------------------------------------
+ * Validate NVRAM files against libvirt VM UUIDs and snapshots
+ * Returns array with 'valid' and 'orphaned' keys
+ * --------------------------------------------------------- */
+function validate_nvram_uuids() {
+ global $libvirt_location;
+ // Connect to libvirt
+ $lv = libvirt_connect('qemu:///system', false);
+ if (!$lv) {
+ die("ERROR: Failed to connect to libvirt\n");
+ }
+
+ // Get all valid VM UUIDs
+ $domains = libvirt_list_domains($lv);
+ if ($domains === false) {
+ die("ERROR: Failed to list domains\n");
+ }
+
+ $valid_uuids = [];
+ $snapshot_dbs = [];
+
+ foreach ($domains as $dom) {
+ $domget = libvirt_domain_lookup_by_name($lv, $dom);
+ if ($domget === false) continue;
+
+ // Use the libvirt function to get UUID string directly
+ $uuid = @libvirt_domain_get_uuid_string($domget);
+ if ($uuid) {
+ $valid_uuids[$uuid] = $dom;
+ // Preload snapshot database for this VM
+ $snapshot_dbs[$dom] = load_snapshot_db($dom);
+ }
+ }
+
+ // Scan NVRAM directory
+ $nvram_dir = $libvirt_location . "/qemu/nvram";
+ if (!is_dir($nvram_dir)) {
+ return ['valid' => [], 'orphaned' => []];
+ }
+
+ $nvram_files = glob("$nvram_dir/*");
+ if ($nvram_files === false || count($nvram_files) === 0) {
+ return ['valid' => [], 'orphaned' => []];
+ }
+
+ $valid = [];
+ $orphaned = [];
+
+ foreach ($nvram_files as $file) {
+ $basename = basename($file);
+
+ // Extract UUID and optional snapshot name from filename
+ // Regular: {UUID}_VARS-pure-efi.fd
+ // Snapshot: {UUID}S{snapshot_name}_VARS-pure-efi.fd
+ if (preg_match('/^([a-f0-9\-]+)(?:S([^_]+))?_VARS/', $basename, $matches)) {
+ $uuid = $matches[1];
+ $snapshot_name = isset($matches[2]) ? $matches[2] : null;
+
+ if (isset($valid_uuids[$uuid])) {
+ $vm_name = $valid_uuids[$uuid];
+ $is_snapshot = $snapshot_name !== null;
+ $snapshot_valid = true;
+
+ // If it's a snapshot, validate against snapshots.db
+ if ($is_snapshot) {
+ $snapshots = $snapshot_dbs[$vm_name] ?? [];
+ $snapshot_valid = isset($snapshots[$snapshot_name]);
+ }
+
+ if ($snapshot_valid) {
+ $valid[] = [
+ 'file' => $file,
+ 'basename' => $basename,
+ 'uuid' => $uuid,
+ 'vm_name' => $vm_name,
+ 'snapshot_name' => $snapshot_name,
+ 'is_snapshot' => $is_snapshot,
+ 'size' => filesize($file)
+ ];
+ } else {
+ $orphaned[] = [
+ 'file' => $file,
+ 'basename' => $basename,
+ 'uuid' => $uuid,
+ 'vm_name' => $vm_name,
+ 'snapshot_name' => $snapshot_name,
+ 'is_snapshot' => true,
+ 'size' => filesize($file),
+ 'reason' => 'snapshot not found in snapshots.db'
+ ];
+ }
+ } else {
+ $orphaned[] = [
+ 'file' => $file,
+ 'basename' => $basename,
+ 'uuid' => $uuid,
+ 'snapshot_name' => $snapshot_name,
+ 'is_snapshot' => $snapshot_name !== null,
+ 'size' => filesize($file),
+ 'reason' => 'VM not found'
+ ];
+ }
+ }
+ }
+
+ return ['valid' => $valid, 'orphaned' => $orphaned];
+}
+
+/* ---------------------------------------------------------
+ * Delete orphaned NVRAM files
+ * --------------------------------------------------------- */
+function delete_orphaned_files($orphaned_files, $dry_run = false) {
+ if (empty($orphaned_files)) {
+ return ['deleted' => 0, 'failed' => 0, 'errors' => []];
+ }
+
+ $deleted = 0;
+ $failed = 0;
+ $errors = [];
+
+ foreach ($orphaned_files as $item) {
+ if (file_exists($item['file'])) {
+ if ($dry_run) {
+ // In dry-run mode, just count as would-be deleted
+ $deleted++;
+ } elseif (@unlink($item['file'])) {
+ $deleted++;
+ } else {
+ $failed++;
+ $errors[] = [
+ 'file' => $item['file'],
+ 'error' => 'Failed to delete'
+ ];
+ }
+ } else {
+ $failed++;
+ $errors[] = [
+ 'file' => $item['file'],
+ 'error' => 'File not found'
+ ];
+ }
+ }
+
+ return ['deleted' => $deleted, 'failed' => $failed, 'errors' => $errors, 'dry_run' => $dry_run];
+}
+
+// Parse command line arguments
+$delete_flag = in_array('--delete', $argv) || in_array('-d', $argv);
+$migrate_flag = in_array('--migrate', $argv) || in_array('-m', $argv);
+$valid_only = in_array('--valid-only', $argv) || in_array('-v', $argv);
+$orphaned_only = in_array('--orphaned-only', $argv) || in_array('-o', $argv);
+$confirm = in_array('--confirm', $argv) || in_array('-y', $argv);
+$dry_run = !$confirm; // Default to dry-run unless --confirm is set
+
+// Run validation and output results
+$result = validate_nvram_uuids();
+
+// Build output based on filters
+if ($valid_only) {
+ $output = ['valid' => $result['valid']];
+} elseif ($orphaned_only) {
+ $output = ['orphaned' => $result['orphaned']];
+} else {
+ $output = [
+ 'valid' => $result['valid'],
+ 'orphaned' => $result['orphaned']
+ ];
+}
+
+// Delete orphaned files if flag is set
+if ($delete_flag && !empty($result['orphaned'])) {
+ $output['deletion_result'] = delete_orphaned_files($result['orphaned'], $dry_run);
+}
+
+// Migrate valid NVRAM files if flag is set
+if ($migrate_flag && !empty($result['valid'])) {
+ $output['migration_result'] = perform_migration($result['valid'], $dry_run);
+}
+
+// Add dry-run flag to output if set
+if ($dry_run) {
+ $output['dry_run'] = true;
+}
+
+echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+
+exit(count($result['orphaned']) === 0 ? 0 : 1);
+
+?>
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
new file mode 100755
index 0000000000..e6e11227e6
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
@@ -0,0 +1,44 @@
+#!/usr/bin/php
+
+
+
+$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
+require_once "$docroot/webGui/include/Helpers.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/fs_helpers.php";
+
+$vmsjson = file_get_contents("/boot/config/plugins/dynamix.vm.manager/vms.json");
+$vms = json_decode($vmsjson,true);
+
+file_put_contents("/tmp/libvirtrestore","");
+foreach ($vms as $vm => $vmdetail) {
+ $to_file = "/etc/libvirt/qemu/$vm.xml";
+ $from_file = $vmdetail['path']."/$vm.xml";
+ #echo " from:$from_file to:$to_file";
+ if (file_exists($from_file)) {
+ $res = copy_if_different($from_file, $to_file, false);
+ $msg = "$vm from:$from_file to:$to_file";
+ if (!empty($res['error'])) {
+ $msg .= " ERROR:" . $res['error'];
+ } elseif (!empty($res['copied'])) {
+ $msg .= " COPIED";
+ } elseif (!empty($res['would_copy'])) {
+ $msg .= " WOULD_COPY";
+ } else {
+ $msg .= " SKIPPED_IDENTICAL";
+ }
+ file_put_contents("/tmp/libvirtrestore", $msg . "\n", FILE_APPEND);
+ } else {
+ file_put_contents("/tmp/libvirtrestore","Nocpy $vm from:$from_file to:$to_file\n",FILE_APPEND);
+ }
+}
+?>
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/savehook.php b/emhttp/plugins/dynamix.vm.manager/scripts/savehook.php
new file mode 100644
index 0000000000..f06e25279d
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/savehook.php
@@ -0,0 +1,12 @@
+#!/usr/bin/env php
+
diff --git a/etc/rc.d/rc.libvirt b/etc/rc.d/rc.libvirt
index ae1c88e7b7..a53c4d237e 100755
--- a/etc/rc.d/rc.libvirt
+++ b/etc/rc.d/rc.libvirt
@@ -247,6 +247,9 @@ libvirtd_start(){
}
libvirtd_stop(){
+ # Save VM locations
+ /usr/local/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
+ #
log "Stopping $DAEMON..."
if [[ ! -f $LIBVIRTD_PIDFILE ]]; then
log "$DAEMON... Already stopped."