diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page index 9cd53c04c..fbb9f2a70 100644 --- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page +++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page @@ -234,7 +234,8 @@ function VMClone(uuid, name){ var edit = box.find('#Edit').prop('checked') ? 'yes' : 'no'; var overwrite = box.find('#Overwrite').prop('checked') ? 'yes' : 'no'; var free = box.find('#Free').prop('checked') ? 'yes' : 'no'; - scripturl = "VMClone.php " + encodeURIComponent("/usr/local/emhttp/plugins/dynamix.vm.manager/include/VMClone.php&" + $.param({action:"clone", name:name, clone:clone, overwrite:overwrite, edit:edit, start:start, free:free})); + var regenmac = box.find('#Regenmac').prop('checked') ? 'yes' : 'no'; + scripturl = "VMClone.php " + encodeURIComponent("/usr/local/emhttp/plugins/dynamix.vm.manager/include/VMClone.php&" + $.param({action:"clone", name:name, clone:clone, overwrite:overwrite, edit:edit, start:start, free:free, regenmac:regenmac})); openVMAction((scripturl),"VM Clone", "dynamix.vm.manager", "loadlist"); box.dialog('close'); }, @@ -572,6 +573,7 @@ _(Snapshot Name)_: _(Start Cloned VM)_: _(Edit VM after clone)_: +_(Regenerate MAC addresses)_: _(Check free space)_: diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php index 35461b16a..92f136c0a 100644 --- a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php +++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php @@ -1243,7 +1243,7 @@ class Array2XML { } function getValidNetworks() { - global $lv; + global $lv,$libvirt_running; $arrValidNetworks = []; exec("ls --indicator-style=none /sys/class/net | grep -Po '^(br|bond|eth|wlan)[0-9]+(\.[0-9]+)?'",$arrBridges); // add 'virbr0' as default first choice @@ -1260,7 +1260,7 @@ class Array2XML { $arrValidNetworks['bridges'] = array_diff($arrBridges, $remove); // This breaks VMSettings.page if libvirt is not running - /* if ($libvirt_running == "yes") { + if ($libvirt_running == "yes") { $arrVirtual = $lv->libvirt_get_net_list($lv->get_connection()); if (($key = array_search('default', $arrVirtual)) !== false) { @@ -1270,7 +1270,7 @@ class Array2XML { array_unshift($arrVirtual, 'default'); $arrValidNetworks['libvirt'] = array_values($arrVirtual); - }*/ + } return $arrValidNetworks; } @@ -1727,7 +1727,7 @@ class Array2XML { return $copypaste; } - function vm_clone($vm, $clone ,$overwrite,$start,$edit, $free, $waitID) { + function vm_clone($vm, $clone ,$overwrite,$start,$edit, $free, $waitID, $regenmac) { global $lv,$domain_cfg,$arrDisplayOptions; /* Clone. @@ -1800,9 +1800,12 @@ class Array2XML { $config["domain"]["name"] = $clone; $config["domain"]["uuid"] = $lv->domain_generate_uuid(); - foreach($config["nic"] as $index => $detail) { - $config["nic"][$index]["mac"] = $lv->generate_random_mac_addr(); - } + if ($regenmac == "yes") { + write("addLog\0".htmlspecialchars(_("Regenerate MACs"))); + foreach($config["nic"] as $index => $detail) { + $config["nic"][$index]["mac"] = $lv->generate_random_mac_addr(); + } + } else write("addLog\0".htmlspecialchars(_("Retain existing MACs"))); $config["domain"]["type"] = "kvm"; $usbs = getVMUSBs($vmxml); @@ -1831,7 +1834,7 @@ class Array2XML { if (!is_dir($clonedir)) { my_mkdir($clonedir,0777,true); } - write("addLog\0".htmlspecialchars("Checking for image files")); + write("addLog\0".htmlspecialchars(_("Checking for image files"))); if ($file_exists && $overwrite != "yes") { write("addLog\0".htmlspecialchars(_("New image file names exist and Overwrite is not allowed"))); return( false); } #Create duplicate files. @@ -1844,8 +1847,18 @@ class Array2XML { $reptgt = str_replace('/mnt/user/', "/mnt/$sourcerealdisk/", $target); $repsrc = str_replace('/mnt/user/', "/mnt/$sourcerealdisk/", $source); } - $cmdstr = "cp --reflink=always '$repsrc' '$reptgt'"; - if ($reflink == true) { $refcmd = $cmdstr; } else {$refcmd = false; } + + $refresult = getFilesystemAndReflinkMode($repsrc); + $refcmdaction = $refresult['reflink_mode']; + $refcmdfs = $refresult['filesystem']; + $cmdstr = "cp --reflink=$refcmdaction '$repsrc' '$reptgt'"; + write("addLog\0".htmlspecialchars(_("Reflink Action")." ".$refcmdaction." "._("for filesystem")." ".$refcmdfs)); + if ($reflink == true) { $refcmd = $cmdstr; } else {$refcmd = false; } + if ($refresult['filesystem'] == "zfs" && $refresult['reflink_mode'] == "skip") { + write("addLog\0".htmlspecialchars(_("Reflink copy not supported on ZFS skipping"))); + $refcmd = false; + } + $cmdstr = "rsync -ahPIXS --out-format=%f --info=flist0,misc0,stats0,name1,progress2 '$repsrc' '$reptgt'"; $error = execCommand_nchan_clone($cmdstr,$target,$refcmd); if (!$error) { write("addLog\0".htmlspecialchars("Image copied failed.")); return( false); } @@ -2936,4 +2949,71 @@ function get_storage_fstype($storage="default") { $fstype = trim(shell_exec(" stat -f -c '%T' $storage")); return $fstype; } + +function getFilesystemAndReflinkMode(string $filePath): array { + // Default return values + $result = [ + 'filesystem' => 'unknown', + 'reflink_mode' => 'always' // safest fallback if unsupported + ]; + + $realPath = realpath($filePath); + if ($realPath === false || !file_exists($realPath)) { + return $result; + } + + $dirname = dirname($realPath); + + // Handle Unraid-style virtual paths + if (strpos($dirname, '/mnt/user/') === 0) { + error_log("Getting real disks\n"); + $realdisk = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION " . escapeshellarg($dirname) . " 2>/dev/null")); + if (!empty($realdisk)) { + $dirname = str_replace('/mnt/user/', "/mnt/$realdisk/", $dirname); + } + } + + // Get filesystem type + $fstype = trim(shell_exec("stat -f -c '%T' " . escapeshellarg($dirname))); + $result['filesystem'] = $fstype; + + // If not ZFS, return early + if ($fstype !== 'zfs') { + return $result; + } + + $result['reflink_mode'] = 'skip'; + + // Get dataset from the file + $dataset = trim(shell_exec("zfs list -H -o name " . escapeshellarg($dirname) . " 2>/dev/null")); + if (empty($dataset)) { + return $result; + } + + // Get pool from dataset + $parts = explode('/', $dataset); + $pool = $parts[0] ?? ''; + if (empty($pool)) { + return $result; + } + + // Check ZFS block_cloning feature + $output = []; + $resultCode = 0; + exec("zpool get -H -o value feature@block_cloning " . escapeshellarg($pool) . " 2>/dev/null", $output, $resultCode); + if ($resultCode !== 0 || empty($output) || (trim($output[0]) !== 'enabled' && trim($output[0]) !== 'active')) { + return $result; + } + + // Check if bclone is enabled at runtime + $bcloneValue = @file_get_contents("/sys/module/zfs/parameters/zfs_bclone_enabled"); + if ($bcloneValue === false || trim($bcloneValue) !== '1') { + return $result; + } + + // If both checks passed, block cloning is supported + $result['reflink_mode'] = 'auto'; + return $result; +} + ?> diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php b/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php index 8862807ba..f198b6c34 100755 --- a/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php +++ b/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php @@ -99,7 +99,7 @@ write("addLog\0".htmlspecialchars("Cloning $name to $clone")); switch ($action) { case "clone": - $rtn = vm_clone($name,$clone,$overwrite,$start,$edit,$free,$waitID) ; + $rtn = vm_clone($name,$clone,$overwrite,$start,$edit,$free,$waitID,$regenmac) ; break ; } write("stop_Wait\0$waitID") ; diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page index 032a7d72b..267460f1a 100644 --- a/emhttp/plugins/dynamix/DashStats.page +++ b/emhttp/plugins/dynamix/DashStats.page @@ -698,6 +698,7 @@ function hideShow() { _(Overwrite)_: _(Start Cloned VM)_: _(Edit VM after clone)_: +_(Regenerate MAC addresses)_: _(Check free space)_: @@ -1464,7 +1465,8 @@ function VMClone(uuid, name){ var edit = box.find('#Edit').prop('checked') ? 'yes' : 'no'; var overwrite = box.find('#Overwrite').prop('checked') ? 'yes' : 'no'; var free = box.find('#Free').prop('checked') ? 'yes' : 'no'; - var scripturl = "VMClone.php " + encodeURIComponent("/usr/local/emhttp/plugins/dynamix.vm.manager/include/VMClone.php&" + $.param({action:"clone", name:name, clone:clone, overwrite:overwrite, edit:edit, start:start, free:free})); + var regenmac = box.find('#Regenmac').prop('checked') ? 'yes' : 'no'; + var scripturl = "VMClone.php " + encodeURIComponent("/usr/local/emhttp/plugins/dynamix.vm.manager/include/VMClone.php&" + $.param({action:"clone", name:name, clone:clone, overwrite:overwrite, edit:edit, start:start, free:free, regenmac:regenmac})); openVMAction((scripturl),"VM Clone", "dynamix.vm.manager", "loadlist"); box.dialog('close'); },