_(Libvirt volume info)_
_(btrfs filesystem show)_:
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_update b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_update
deleted file mode 100755
index 0c1d2f9130..0000000000
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_update
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-# Process settings update.
-if [ -f /boot/config/domain.cfg ]; then
- . /boot/config/domain.cfg
- if [ "$SERVICE" == "enable" ]; then
- /etc/rc.d/rc.libvirt start
- else
- /etc/rc.d/rc.libvirt stop
- fi
-fi
From 28ec84805a0e3c1e5a1453ef1272e10a81262e5e Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Sat, 19 Apr 2025 18:40:16 +0100
Subject: [PATCH 08/18] revert rc.libvirt changes
---
etc/rc.d/rc.libvirt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/etc/rc.d/rc.libvirt b/etc/rc.d/rc.libvirt
index 60c1bdf8ba..013220079e 100755
--- a/etc/rc.d/rc.libvirt
+++ b/etc/rc.d/rc.libvirt
@@ -173,7 +173,10 @@ version(){
libvirtd_start(){
log "Starting $DAEMON..."
- if [[ -f $LIBVIRTD_PIDFILE ]]; then
+ if ! mountpoint /etc/libvirt &>/dev/null; then
+ log "$DAEMON... No image mounted at /etc/libvirt."
+ exit 1
+ elif [[ -f $LIBVIRTD_PIDFILE ]]; then
log "$DAEMON... Already started."
return 1
fi
From de53cf49e28892bc8a6457d31978884ebedece8a Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Sun, 20 Apr 2025 10:19:42 +0100
Subject: [PATCH 09/18] Initial moving libvirt image to folder and vice versa.
Default set to folder.
---
.../dynamix.vm.manager/VMSettings.page | 18 ++++++
.../dynamix.vm.manager/scripts/libvirt_init | 58 +++++++++++++++++++
.../dynamix.vm.manager/scripts/libvirtconfig | 3 +-
etc/rc.d/rc.libvirt | 2 +-
4 files changed, 79 insertions(+), 2 deletions(-)
diff --git a/emhttp/plugins/dynamix.vm.manager/VMSettings.page b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
index 9d331c9a6f..1a5851576d 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMSettings.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
@@ -110,6 +110,12 @@ _(Libvirt storage location)_:
:vms_libvirt_volume_help:
+_(Libvirt secondary storage location)_:
+: =htmlspecialchars($domain_cfg['IMAGE_FILE_SECONDARY'])."Test"?>
+
+:vms_libvirt_secondary_volume_help:
+
+
_(Libvirt vdisk size)_:
:
_(GB)_
@@ -125,6 +131,14 @@ _(Libvirt storage location)_:
:vms_libvirt_location_help:
+_(Libvirt secondary storage location)_:
+:
+
_(Modify with caution: unable to validate path until Array is Started)_
+
_(Path does not exist)_
+
+
+:vms_libvirt_secondary_location_help:
+
_(Default VM storage path)_:
:
@@ -428,6 +442,9 @@ $(function(){
$("#IMAGE_FILE").fileTreeAttach(null, null, function(folder) {
$("#IMAGE_FILE").val(folder + 'libvirt.img').change();
});
+ $("#IMAGE_FILE_SECONDARY").fileTreeAttach(null, null, function(folder) {
+ $("#IMAGE_FILE_SECONDARY").val(folder + 'libvirt.img').change();
+ });
$('#domaindir').fileTreeAttach();
$('#mediadir').fileTreeAttach();
$('#winvirtio').fileTreeAttach();
@@ -451,6 +468,7 @@ $(function(){
$("#SERVICE").prop("disabled", checked).val('disable');
$("#IMAGE_SIZE").prop("disabled", checked);
$("#IMAGE_FILE").prop("disabled", checked).val("=$domain_cfg['IMAGE_FILE']?>");
+ $("#IMAGE_FILE_SECONDARY").prop("disabled");
$("#domaindir").prop("disabled", checked);
$("#mediadir").prop("disabled", checked);
$("#winvirtio_select").prop("disabled", checked);
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
index 4b98d16f0c..1189383f11 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"
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig
index d09296f9ae..b588cf89b2 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/etc/rc.d/rc.libvirt b/etc/rc.d/rc.libvirt
index 013220079e..e41bc61b61 100755
--- a/etc/rc.d/rc.libvirt
+++ b/etc/rc.d/rc.libvirt
@@ -173,7 +173,7 @@ version(){
libvirtd_start(){
log "Starting $DAEMON..."
- if ! mountpoint /etc/libvirt &>/dev/null; then
+ if ! mountpoint /etc/libvirt &>/dev/null; then
log "$DAEMON... No image mounted at /etc/libvirt."
exit 1
elif [[ -f $LIBVIRTD_PIDFILE ]]; then
From 3ffebdfb6e5e40d5fda67330af76c76de05142b9 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Sun, 20 Apr 2025 11:26:10 +0100
Subject: [PATCH 10/18] Update help text
---
emhttp/languages/en_US/helptext.txt | 12 ++++++++++--
emhttp/plugins/dynamix.vm.manager/VMSettings.page | 5 ++---
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/emhttp/languages/en_US/helptext.txt b/emhttp/languages/en_US/helptext.txt
index b878ec9afa..b11c1c65b0 100644
--- a/emhttp/languages/en_US/helptext.txt
+++ b/emhttp/languages/en_US/helptext.txt
@@ -1618,7 +1618,11 @@ Stop VMs from Autostarting\Starting when VM Manager starts or open is run from t
:end
:vms_libvirt_volume_help:
-This is the libvirt volume.
+This is the libvirt volume/directory.
+:end
+
+:vms_libvirt_secondary_volume_help:
+This is a location for storing previous versions of xml and nvram at change.
:end
:vms_libvirt_vdisk_size_help:
@@ -1627,7 +1631,11 @@ To resize an existing image file, specify the new size here. Next time the Libvi
:end
:vms_libvirt_location_help:
-You must specify an image file for Libvirt. The system will automatically create this file when the Libvirt service is first started.
+You must specify an image file/directory for Libvirt. The system will automatically create this file/directory when the Libvirt service is first started.
+:end
+
+:vms_libvirt_secondary_location_help:
+This is a directory for storing previous versions of xml and nvram at change. Does not need to be specified.
:end
:vms_libvirt_storage_help:
diff --git a/emhttp/plugins/dynamix.vm.manager/VMSettings.page b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
index 1a5851576d..0abf50b3c9 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMSettings.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
@@ -123,7 +123,7 @@ _(Libvirt vdisk size)_:
:vms_libvirt_vdisk_size_help:
_(Libvirt storage location)_:
-:
+:
_(Modify with caution: unable to validate path until Array is Started)_
_(Path does not exist)_
@@ -132,9 +132,8 @@ _(Libvirt storage location)_:
:vms_libvirt_location_help:
_(Libvirt secondary storage location)_:
-:
+:
_(Modify with caution: unable to validate path until Array is Started)_
-
_(Path does not exist)_
:vms_libvirt_secondary_location_help:
From 7b033823528e74667aa009056f29c5e7a41d4bb2 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Tue, 6 May 2025 20:18:40 +0100
Subject: [PATCH 11/18] Remove secondary path.
---
emhttp/plugins/dynamix.vm.manager/VMSettings.page | 7 -------
1 file changed, 7 deletions(-)
diff --git a/emhttp/plugins/dynamix.vm.manager/VMSettings.page b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
index f00d792524..11bb44245f 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMSettings.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
@@ -169,13 +169,6 @@ _(Libvirt storage location)_:
:vms_libvirt_location_help:
-_(Libvirt secondary storage location)_:
-:
-
_(Modify with caution: unable to validate path until Array is Started)_
-
-
-:vms_libvirt_secondary_location_help:
-
_(Default VM storage path)_:
:
From fca221a93e3ad6013756708d9e131bfd4b47b90f Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Tue, 6 May 2025 20:21:12 +0100
Subject: [PATCH 12/18] Remove secondary code.
---
emhttp/plugins/dynamix.vm.manager/VMSettings.page | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/emhttp/plugins/dynamix.vm.manager/VMSettings.page b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
index 11bb44245f..03c4235d43 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMSettings.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
@@ -146,12 +146,6 @@ _(Libvirt storage location)_:
:vms_libvirt_volume_help:
-_(Libvirt secondary storage location)_:
-: =htmlspecialchars($domain_cfg['IMAGE_FILE_SECONDARY'])."Test"?>
-
-:vms_libvirt_secondary_volume_help:
-
-
_(Libvirt vdisk size)_:
:
_(GB)_
@@ -482,9 +476,6 @@ $(function(){
$("#IMAGE_FILE").fileTreeAttach(null, null, function(folder) {
$("#IMAGE_FILE").val(folder + 'libvirt.img').change();
});
- $("#IMAGE_FILE_SECONDARY").fileTreeAttach(null, null, function(folder) {
- $("#IMAGE_FILE_SECONDARY").val(folder + 'libvirt.img').change();
- });
$('#domaindir').fileTreeAttach();
$('#mediadir').fileTreeAttach();
$('#winvirtio').fileTreeAttach();
@@ -508,7 +499,6 @@ $(function(){
$("#SERVICE").prop("disabled", checked).val('disable');
$("#IMAGE_SIZE").prop("disabled", checked);
$("#IMAGE_FILE").prop("disabled", checked).val("=$domain_cfg['IMAGE_FILE']?>");
- $("#IMAGE_FILE_SECONDARY").prop("disabled");
$("#domaindir").prop("disabled", checked);
$("#mediadir").prop("disabled", checked);
$("#winvirtio_select").prop("disabled", checked);
From 72562977caea2b9cd28bf080018e5e0faaea98c7 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Wed, 7 Jan 2026 10:59:08 +0000
Subject: [PATCH 13/18] Create location file.
---
.../dynamix.vm.manager/VMSettings.page | 8 +-
.../scripts/libvirtlocation | 137 ++++++++++++++++++
etc/rc.d/rc.libvirt | 3 +
3 files changed, 145 insertions(+), 3 deletions(-)
create mode 100644 emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
diff --git a/emhttp/plugins/dynamix.vm.manager/VMSettings.page b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
index ce191b0436..8ce9d5dee8 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMSettings.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMSettings.page
@@ -162,14 +162,15 @@ _(Libvirt storage location)_:
:vms_libvirt_volume_help:
-
-_(Libvirt vdisk size)_ (_(GB)_):
+
+
+ _(Libvirt vdisk size)_ (_(GB)_):
:
:vms_libvirt_vdisk_size_help:
_(Libvirt storage location)_:
-:
+:
@@ -180,6 +181,7 @@ _(Libvirt storage location)_:
:vms_libvirt_location_help:
+
_(Default VM storage path)_:
:
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
new file mode 100644
index 0000000000..e96897be22
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
@@ -0,0 +1,137 @@
+#!/usr/bin/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 = '/mnt/user/' . $metadata_storage;
+ $storage_name = $metadata_storage;
+ }
+
+ /* Shell-safe path (VM name quoted) */
+ $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[] = [
+ 'name' => $vm_name,
+ 'uuid' => $uuid,
+ 'storage' => $storage_name,
+ 'path' => $path,
+ 'exists' => $exists,
+ ];
+}
+
+/* ---------------------------------------------------------
+ * Output
+ * --------------------------------------------------------- */
+#print_r($vms);
+
+file_put_contents("/boot/config/plugins/dynamix.vm.manager/vms.json",json_encode($vms,JSON_PRETTY_PRINT));
+?>
diff --git a/etc/rc.d/rc.libvirt b/etc/rc.d/rc.libvirt
index ae1c88e7b7..06117661ef 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/libvirtlocation
+ #
log "Stopping $DAEMON..."
if [[ ! -f $LIBVIRTD_PIDFILE ]]; then
log "$DAEMON... Already stopped."
From 00ed5d2f2ebec8c864346ff97137d9c60c4a9235 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Wed, 7 Jan 2026 10:59:45 +0000
Subject: [PATCH 14/18] Make script executable
---
emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
old mode 100644
new mode 100755
From d13e6b202a0cf02a651582f79d839ed618f3fcb2 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Wed, 7 Jan 2026 14:11:27 +0000
Subject: [PATCH 15/18] XML movements
---
.../scripts/{libvirtlocation => libvirtcopy} | 21 +++++++++++---
.../dynamix.vm.manager/scripts/libvirtrestore | 28 +++++++++++++++++++
.../dynamix.vm.manager/scripts/savehook.php | 12 ++++++++
etc/rc.d/rc.libvirt | 2 +-
4 files changed, 58 insertions(+), 5 deletions(-)
rename emhttp/plugins/dynamix.vm.manager/scripts/{libvirtlocation => libvirtcopy} (84%)
mode change 100755 => 100644
create mode 100644 emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
create mode 100644 emhttp/plugins/dynamix.vm.manager/scripts/savehook.php
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
old mode 100755
new mode 100644
similarity index 84%
rename from emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
rename to emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
index e96897be22..222ce20051
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
@@ -108,22 +108,23 @@ foreach ($domains as $dom) {
$storage_name = $metadata_storage;
}
- /* Shell-safe path (VM name quoted) */
+
$path = $path_root
- ? $path_root . '/"' . $vm_name . '"'
+ ? $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[] = [
- 'name' => $vm_name,
+ $vms[$vm_name] = [
'uuid' => $uuid,
'storage' => $storage_name,
'path' => $path,
+ 'path_shell' => $path ? escapeshellarg($path) : null,
'exists' => $exists,
];
}
@@ -134,4 +135,16 @@ foreach ($domains as $dom) {
#print_r($vms);
file_put_contents("/boot/config/plugins/dynamix.vm.manager/vms.json",json_encode($vms,JSON_PRETTY_PRINT));
+
+
+foreach ($vms as $vm => $vmdetail) {
+ file_put_contents("/tmp/Stopcopy","");
+ $from_file = "/etc/libvirt/qemu/$vm.xml";
+ $to_file = $vmdetail['path']."/$vm.xml";
+ #echo " from:$from_file to:$to_file";
+ if ($vmdetail['exists']) {
+ file_put_contents("/tmp/Stopcopy","$vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_file";
+ #copy($from_file,$to_file);
+ } 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/libvirtrestore b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
new file mode 100644
index 0000000000..e153fc6bd5
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
@@ -0,0 +1,28 @@
+#!/usr/bin/php
+
+
+
+$vmsjson = file_get_contents("/boot/config/plugins/dynamix.vm.manager/vms.json");
+$vms = json_decode($vmsjson,true);
+
+foreach ($vms as $vm => $vmdetail) {
+ file_put_contents("/tmp/Stopcopy","");
+ $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)) {
+ file_put_contents("/tmp/libvirtrestore","$vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_file";
+ #copy($from_file,$to_file);
+ } else file_put_contents("/tmp/libvirtrestore","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/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 06117661ef..a53c4d237e 100755
--- a/etc/rc.d/rc.libvirt
+++ b/etc/rc.d/rc.libvirt
@@ -248,7 +248,7 @@ libvirtd_start(){
libvirtd_stop(){
# Save VM locations
- /usr/local/emhttp/plugins/dynamix.vm.manager/scripts/libvirtlocation
+ /usr/local/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
#
log "Stopping $DAEMON..."
if [[ ! -f $LIBVIRTD_PIDFILE ]]; then
From a41622bd7c288a65d05e02c78102671549c62af2 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Wed, 7 Jan 2026 14:13:16 +0000
Subject: [PATCH 16/18] Make script executable
---
emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy | 0
emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore | 0
2 files changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
mode change 100644 => 100755 emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
old mode 100644
new mode 100755
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
old mode 100644
new mode 100755
From 092af901af850f392e1d595c9b0fd668b71d4213 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Thu, 8 Jan 2026 15:56:19 +0000
Subject: [PATCH 17/18] Udates to XML movement.
---
emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init | 4 ++++
emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy | 8 ++++----
emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore | 2 +-
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
index 1189383f11..a9643d538b 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirt_init
@@ -94,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/libvirtcopy b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
index 222ce20051..1acf75c066 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
@@ -104,7 +104,7 @@ foreach ($domains as $dom) {
$storage_name = 'default';
} else {
/* Explicit Unraid pool */
- $path_root = '/mnt/user/' . $metadata_storage;
+ $path_root = str_replace("/mnt/user/","/mnt/$metadata_storage/",$default_domain_dir);
$storage_name = $metadata_storage;
}
@@ -133,12 +133,12 @@ foreach ($domains as $dom) {
* 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) {
- file_put_contents("/tmp/Stopcopy","");
+
$from_file = "/etc/libvirt/qemu/$vm.xml";
$to_file = $vmdetail['path']."/$vm.xml";
#echo " from:$from_file to:$to_file";
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
index e153fc6bd5..89282531e6 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
@@ -15,8 +15,8 @@
$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) {
- file_put_contents("/tmp/Stopcopy","");
$to_file = "/etc/libvirt/qemu/$vm.xml";
$from_file = $vmdetail['path']."/$vm.xml";
#echo " from:$from_file to:$to_file";
From 6d7c50cabda34eb680a907019bc76872eb3ef9f0 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Fri, 9 Jan 2026 16:17:42 +0000
Subject: [PATCH 18/18] Updates
---
.../dynamix.vm.manager/include/fs_helpers.php | 99 ++++
.../dynamix.vm.manager/scripts/libvirtcopy | 23 +-
.../dynamix.vm.manager/scripts/libvirtmigrate | 498 ++++++++++++++++++
.../dynamix.vm.manager/scripts/libvirtrestore | 22 +-
4 files changed, 636 insertions(+), 6 deletions(-)
create mode 100644 emhttp/plugins/dynamix.vm.manager/include/fs_helpers.php
create mode 100644 emhttp/plugins/dynamix.vm.manager/scripts/libvirtmigrate
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/libvirtcopy b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
index 1acf75c066..19936df874 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtcopy
@@ -12,6 +12,13 @@
?>
+/* ---------------------------------------------------------
+ * 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
* --------------------------------------------------------- */
@@ -141,10 +148,20 @@ 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";
+ #echo " from:$from_file to:$to_file\n";
if ($vmdetail['exists']) {
- file_put_contents("/tmp/Stopcopy","$vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_file";
- #copy($from_file,$to_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/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
index 89282531e6..e6e11227e6 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtrestore
@@ -12,6 +12,10 @@
?>
+$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);
@@ -21,8 +25,20 @@ foreach ($vms as $vm => $vmdetail) {
$from_file = $vmdetail['path']."/$vm.xml";
#echo " from:$from_file to:$to_file";
if (file_exists($from_file)) {
- file_put_contents("/tmp/libvirtrestore","$vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_file";
- #copy($from_file,$to_file);
- } else file_put_contents("/tmp/libvirtrestore","Nocpy $vm from:$from_file to:$to_file\n",FILE_APPEND); #echo " from:$from_file to:$to_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);
+ }
}
?>