From 66874ef8f211cf6390f237ae9f017bbc06a0cf97 Mon Sep 17 00:00:00 2001
From: SimonFair <39065407+SimonFair@users.noreply.github.com>
Date: Sat, 30 Dec 2023 15:51:53 +0000
Subject: [PATCH] Add Support for File system level snapshots for VMs.
---
.../dynamix.vm.manager/VMMachines.page | 9 +-
.../dynamix.vm.manager/include/VMMachines.php | 8 +-
.../dynamix.vm.manager/include/VMajax.php | 4 +-
.../dynamix.vm.manager/include/libvirt.php | 20 +++-
.../include/libvirt_helpers.php | 94 ++++++++++++++++---
.../javascript/vmmanager.js | 11 ++-
emhttp/plugins/dynamix/include/Helpers.php | 21 +++++
7 files changed, 141 insertions(+), 26 deletions(-)
diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page
index f313f4b4d..53eb2d069 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page
@@ -238,12 +238,14 @@ function VMClone(uuid, name){
});
dialogStyle();
}
-function selectsnapshot(uuid, name ,snaps, opt, getlist,state){
+function selectsnapshot(uuid, name ,snaps, opt, getlist,state ,fstype){
var box = $("#dialogWindow");
box.html($("#templatesnapshot"+opt).html());
const capopt = opt.charAt(0).toUpperCase() + opt.slice(1);
var optiontext = capopt + " _(Snapshot)_";
+ //var fstype = "ZFS";
box.find('#VMName').html(name);
+ box.find('#fstype').html(fstype);
box.find('#targetsnap').val(snaps);
box.find('#targetsnapl').html(snaps);
if (getlist) {
@@ -282,9 +284,11 @@ function selectsnapshot(uuid, name ,snaps, opt, getlist,state){
}
if (opt == "create") {
free = box.find('#targetsnapfspc').prop('checked') ? 'yes' : 'no';
+ fstypeuse = box.find('#targetsnapfstype').prop('checked') ? 'yes' : 'no';
+ if (fstypeuse == "no") fstype ="QEMU";
desc = box.find("#targetsnapdesc").prop('value');
}
- ajaxVMDispatch({action:"snap-" + opt +'-external', uuid:uuid, snapshotname:target, remove:remove, free:free, removemeta:removemeta, keep:keep, desc:desc}, "loadlist");
+ ajaxVMDispatch({action:"snap-" + opt +'-external', uuid:uuid, snapshotname:target, remove:remove, free:free, removemeta:removemeta, keep:keep, desc:desc, fstype:fstype}, "loadlist");
box.dialog('close');
},
"_(Cancel)_": function(){
@@ -492,6 +496,7 @@ $(function() {
| _(VM Name)_: | |
| _(Snapshot Name)_: | _(Check free space)_: |
| _(Description )_: | |
+| _(FS Native Snapshot )_: | _(Unchecked will use QEMU External Snapshot)_ |
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
index 2577382bd..6319a2212 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
@@ -108,7 +108,9 @@ foreach ($vms as $vm) {
}
unset($dom);
if (!isset($domain_cfg["CONSOLE"])) $vmrcconsole = "web" ; else $vmrcconsole = $domain_cfg["CONSOLE"] ;
- $menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log), $vmrcconsole,$vmpreview);
+ if ($diskcnt > 0) $fstype = $lv->get_disk_fstype($res); else $fstype="QEMU";
+ #$fstype = "ZFS";
+ $menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log),addslashes($fstype), $vmrcconsole,$vmpreview);
$kvm[] = "kvm.push({id:'$uuid',state:'$state'});";
switch ($state) {
case 'running':
@@ -130,7 +132,7 @@ foreach ($vms as $vm) {
}
/* VM information */
- if ($snapshots != null) $snapshotstr = '('._('Snapshots').': '.count($snapshots).')'; else $snapshotstr = '('._('Snapshots').': '._('None').')';
+ if ($snapshots != null) $snapshotstr = '('._('Snapshots').': '.count($snapshots)." / Type: $fstype)"; else $snapshotstr = '('._('Snapshots').': '._('None')." / Type: $fstype)";
$cdbus = $cdbus2 = $cdfile = $cdfile2 = "";
$cdromcount = 0;
foreach ($cdroms as $arrCD) {
@@ -280,7 +282,7 @@ foreach ($vms as $vm) {
$snapshotmemory = _(ucfirst($snapshot["memory"]["@attributes"]["snapshot"]));
$snapshotparent = $snapshot["parent"] ? $snapshot["parent"] : "None";
$snapshotdatetime = my_time($snapshot["creationtime"],"Y-m-d" )."
".my_time($snapshot["creationtime"],"H:i:s");
- $snapmenu = sprintf("onclick=\"addVMSnapContext('%s','%s','%s','%s','%s','%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,$snapshot["name"],$vmpreview);
+ $snapmenu = sprintf("onclick=\"addVMSnapContext('%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,$snapshot["name"],$snapshot["method"],$vmpreview);
echo "| $tab|__ ",$snapshot["name"]," | $snapshotdesc | $snapshotdatetime | $snapshotstate | $snapshotparent | $snapshotmemory |
";
$tab .=" ";
}
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
index 178f5570f..655ad4c78 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
@@ -338,7 +338,7 @@ case 'snap-create':
case 'snap-create-external':
requireLibvirt();
- $arrResponse = vm_snapshot($domName,$_REQUEST['snapshotname'],$_REQUEST['desc'],$_REQUEST['free']) ;
+ $arrResponse = vm_snapshot($domName,$_REQUEST['snapshotname'],$_REQUEST['desc'],$_REQUEST['free'],$_REQUEST['fstype']) ;
break;
case 'snap-images':
@@ -396,7 +396,7 @@ case 'disk-create':
$driver = $_REQUEST['driver'];
$size = str_replace(["KB","MB","GB","TB","PB", " ", ","], ["K","M","G","T","P", "", ""], strtoupper($_REQUEST['size']));
$dir = dirname($disk);
- if (!is_dir($dir)) mkdir($dir);
+ if (!is_dir($dir)) my_vmmkdir($dir);
// determine the actual disk if user share is being used
$dir = transpose_user_path($dir);
#@exec("chattr +C -R ".escapeshellarg($dir)." >/dev/null");
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
index 847077d9b..30f70af02 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
@@ -175,7 +175,7 @@
// create folder if needed
if (!is_dir($strImgFolder)) {
- mkdir($strImgFolder, 0777, true);
+ my_mkdir($strImgFolder);
chown($strImgFolder, 'nobody');
chgrp($strImgFolder, 'users');
}
@@ -191,7 +191,7 @@
// create parent folder if needed
if (!is_dir($path_parts['dirname'])) {
- mkdir($path_parts['dirname'], 0777, true);
+ my_mkdir($path_parts['dirname']);
chown($path_parts['dirname'], 'nobody');
chgrp($path_parts['dirname'], 'users');
}
@@ -216,7 +216,7 @@
// create folder if needed
$strImgRawLocationParent = dirname($strImgRawLocationPath);
if (!is_dir($strImgRawLocationParent)) {
- mkdir($strImgRawLocationParent, 0777, true);
+ my_mkdir($strImgRawLocationParent);
chown($strImgRawLocationParent, 'nobody');
chgrp($strImgRawLocationParent, 'users');
}
@@ -1409,6 +1409,20 @@
return $ret;
}
+ function get_disk_fstype($domain) {
+ $dom = $this->get_domain_object($domain);
+ $tmp = $this->get_disk_stats($dom);
+ $dirname = transpose_user_path($tmp[0]['file']);
+ $pathinfo = pathinfo($dirname);
+ $parent = $pathinfo["dirname"];
+ $fstype = strtoupper(trim(shell_exec(" stat -f -c '%T' $parent")));
+ if ($fstype != "ZFS") $fstype = "QEMU";
+ #if ($fstype != "ZFS" && $fstype != "BTRFS") $fstype = "QEMU";
+ unset($tmp);
+
+ return $fstype;
+ }
+
function format_size($value, $decimals, $unit='?') {
if ($value == '-')
return 'unknown';
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
index 21fb48052..b8322d33e 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
@@ -1681,7 +1681,7 @@ private static $encoding = 'UTF-8';
if ($storage == "default") $clonedir = $domain_cfg['DOMAINDIR'].$clone ; else $clonedir = str_replace('/mnt/user/', "/mnt/$storage/", $domain_cfg['DOMAINDIR']).$clone;
if (!is_dir($clonedir)) {
- mkdir($clonedir,0777,true) ;
+ my_mkdir($clonedir) ;
chown($clonedir, 'nobody');
chgrp($clonedir, 'users');
}
@@ -1732,17 +1732,33 @@ private static $encoding = 'UTF-8';
return $snaps ;
}
- function write_snapshots_database($vm,$name,$method="QEMU") {
+ function write_snapshots_database($vm,$name,$state,$desc,$method="QEMU") {
global $lv ;
$dbpath = "/etc/libvirt/qemu/snapshot/$vm" ;
if (!is_dir($dbpath)) mkdir($dbpath) ;
+ $noxml = "";
$snaps_json = file_get_contents($dbpath."/snapshots.db") ;
$snaps = json_decode($snaps_json,true) ;
$snapshot_res=$lv->domain_snapshot_lookup_by_name($vm,$name) ;
+ if (!$snapshot_res) {
+ # Manual Snap no XML
+ if ($state == "shutoff" && ($method == "ZFS" || $method == "BTRFS")) {
+ # Create Snapshot info
+ $vmsnap = $name;
+ $snaps[$vmsnap]["name"]= $name;
+ $snaps[$vmsnap]["parent"]= "Base" ;
+ $snaps[$vmsnap]["state"]= "shutoff";
+ $snaps[$vmsnap]["desc"]= $desc;
+ $snaps[$vmsnap]["memory"]= ['@attributes' => ['snapshot' => 'no']];
+ $snaps[$vmsnap]["creationtime"]= date("U");
+ $snaps[$vmsnap]["method"]= $method;
+ $noxml = "noxml";
+ }
+ } else {
$snapshot_xml=$lv->domain_snapshot_get_xml($snapshot_res) ;
$a = simplexml_load_string($snapshot_xml) ;
$a = json_encode($a) ;
- $b= json_decode($a, TRUE);
+ $b = json_decode($a, TRUE);
$vmsnap = $b["name"] ;
$snaps[$vmsnap]["name"]= $b["name"];
$snaps[$vmsnap]["parent"]= $b["parent"] ;
@@ -1751,6 +1767,7 @@ private static $encoding = 'UTF-8';
$snaps[$vmsnap]["memory"]= $b["memory"];
$snaps[$vmsnap]["creationtime"]= $b["creationTime"];
$snaps[$vmsnap]["method"]= $method;
+ }
$disks =$lv->get_disk_stats($vm) ;
foreach($disks as $disk) {
@@ -1773,11 +1790,12 @@ private static $encoding = 'UTF-8';
$snaps[$vmsnap]["parent"] = str_replace("qcow2",'',$snaps[$vmsnap]["parent"]) ;
if (isset($parentfind[1]) && !isset($parentfind[2])) $snaps[$vmsnap]["parent"]="Base" ;
- if (array_key_exists(0 , $b["disks"]["disk"])) $snaps[$vmsnap]["disks"]= $b["disks"]["disk"]; else $snaps[$vmsnap]["disks"][0]= $b["disks"]["disk"];
+ if (isset($b)) if (array_key_exists(0 , $b["disks"]["disk"])) $snaps[$vmsnap]["disks"]= $b["disks"]["disk"]; else $snaps[$vmsnap]["disks"][0]= $b["disks"]["disk"];
$value = json_encode($snaps,JSON_PRETTY_PRINT) ;
file_put_contents($dbpath."/snapshots.db",$value) ;
+ return $noxml;
}
function refresh_snapshots_database($vm) {
@@ -1847,8 +1865,33 @@ private static $encoding = 'UTF-8';
return true ;
}
+ function my_vmmkdir($dirname) {
+ $pathinfo = pathinfo($dirname);
+ $parent = $pathinfo["dirname"];
+ $userPathFound = strpos($dirname,"/mnt/user");
+ $realdir = $dirname;
+ if ($userPathFound !== false) {
+ $realLocation = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg("$parent")));
+ $realdir = str_replace("/mnt/user","/mnt/$realLocation",$dirname);
+ $parent = dirname($realdir);
+ }
+ $fstype = trim(shell_exec(" stat -f -c '%T' $parent"));
+ switch ($fstype) {
+ case "zfs":
+ $zfsdataset = trim(shell_exec("zfs list -H -o name $parent")) ;
+ shell_exec("zfs create $zfsdataset/{$pathinfo['filename']}");
+ break;
+ case "btrfs":
+ shell_exec("btrfs subvolume create $realdir");
+ break;
+ default:
+ mkdir($realdir, 0777, true);
+ break;
+ }
+}
- function vm_snapshot($vm,$snapshotname, $snapshotdesc, $free = "yes", $memorysnap = "yes") {
+
+ function vm_snapshot($vm,$snapshotname, $snapshotdesc, $free = "yes", $method = "QEMU", $memorysnap = "yes") {
global $lv ;
#Get State
@@ -1876,7 +1919,14 @@ private static $encoding = 'UTF-8';
$dirpath= str_replace('/mnt/user/', "/mnt/$storagelocation/", $dirpath);
}
$filenew = $dirpath.'/'.$pathinfo["filename"].'.'.$name.'qcow2' ;
- $diskspec .= " --diskspec '".$disk["device"]."',snapshot=external,file='".$filenew."'" ;
+ switch ($method) {
+ case "QEMU" :
+ $diskspec .= " --diskspec '".$disk["device"]."',snapshot=external,file='".$filenew."'" ;
+ break;
+ case "ZFS":
+ case "BTRFS":
+ $diskspec .= " --diskspec '".$disk["device"]."',snapshot=manual " ;
+ }
$capacity = $capacity + $disk["capacity"] ;
}
@@ -1884,7 +1934,7 @@ private static $encoding = 'UTF-8';
$mem = $lv->domain_get_memory_stats($vm) ;
$memory = $mem[6] ;
- if ($memorysnap = "yes") $memspec = ' --memspec "'.$dirpath.'/memory'.$name.'.mem",snapshot=external' ; else $memspec = "" ;
+ if ($memorysnap == "yes") $memspec = ' --memspec "'.$dirpath.'/memory'.$name.'.mem",snapshot=external' ; else $memspec = "" ;
$cmdstr = "virsh snapshot-create-as '$vm' --name '$name' $snapshotdesc --atomic" ;
@@ -1911,16 +1961,36 @@ private static $encoding = 'UTF-8';
if ($state == "running") exec("virsh dumpxml '$vm' > ".escapeshellarg($xmlfile),$outxml,$rtnxml) ;
$output= [] ;
- $test = false ;
- if ($test) exec($cmdstr." --print-xml 2>&1",$output,$return) ; else exec($cmdstr." 2>&1",$output,$return) ;
+ #$test = false ;
+ #if ($test) exec($cmdstr." --print-xml 2>&1",$output,$return) ; else exec($cmdstr." 2>&1",$output,$return) ;
+
+ switch ($method) {
+ case "ZFS":
+ # Create ZFS Snapshot
+ #$zfsdataset = "vmpoolzfs/domains3/Arch3";
+ #stat -f -c '%T' /mnt/vmpoolzfs/domains2/Arch3
+ if ($state == "running") exec($cmdstr." 2>&1",$output,$return);
+ $zfsdataset = trim(shell_exec("zfs list -H -o name -r $dirpath")) ;
+ $fssnapcmd = " zfs snapshot $zfsdataset@$name";
+ shell_exec($fssnapcmd);
+ # if running resume.
+ if ($state == "running") $lv->domain_resume($vm);
+ break;
+ case "BTRFS":
+ # Create BTRFS Snapshot
+ break;
+ default:
+ # No Action
+ exec($cmdstr." 2>&1",$output,$return);
+ }
if (strpos(" ".$output[0],"error") ) {
$arrResponse = ['error' => substr($output[0],6) ] ;
} else {
$arrResponse = ['success' => true] ;
- write_snapshots_database("$vm","$name") ;
+ $ret = write_snapshots_database("$vm","$name",$state,$snapshotdesc,$method) ;
#remove meta data
- $ret = $lv->domain_snapshot_delete($vm, "$name" ,2) ;
+ if ($ret != "noxml") $ret = $lv->domain_snapshot_delete($vm, "$name" ,2) ;
}
return $arrResponse ;
@@ -1980,12 +2050,14 @@ private static $encoding = 'UTF-8';
if ($diskname == "hda" || $diskname == "hdb") continue ;
$path = $disk["source"]["@attributes"]["file"] ;
if (is_file($path) && $action == "yes") unlink("$path") ;
+ file_put_contents("/tmp/rmvsnaps",$path,FILE_APPEND);
$item = array_search($path,$snapslist[$snap]['backing']["r".$diskname]) ;
$item++ ;
while($item > 0)
{
if (!isset($snapslist[$snap]['backing']["r".$diskname][$item])) break ;
$newpath = $snapslist[$snap]['backing']["r".$diskname][$item] ;
+ file_put_contents("/tmp/rmvsnaps",$newpath,FILE_APPEND);
if (is_file($newpath) && $action == "yes") unlink("$newpath") ;
$item++ ;
}
diff --git a/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js b/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js
index 588cc6d11..e8c662aa9 100644
--- a/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js
+++ b/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js
@@ -62,7 +62,7 @@ function ajaxVMDispatchconsoleRV(params, spin){
}
},'json');
}
-function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, console="web", preview=false){
+function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, fstype="QEMU",console="web", preview=false){
var opts = [];
var path = location.pathname;
var x = path.indexOf("?");
@@ -110,7 +110,7 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
opts.push({text:_("Create Snapshot"), icon:"fa-clone", action:function(e) {
e.preventDefault();
- selectsnapshot(uuid , name, "--generate" , "create",false,state) ;
+ selectsnapshot(uuid , name, "--generate" , "create",false,state,fstype) ;
}});
} else if (state == "pmsuspended") {
opts.push({text:_("Resume"), icon:"fa-play", action:function(e) {
@@ -165,7 +165,7 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
opts.push({divider:true});
opts.push({text:_("Create Snapshot"), icon:"fa-clone", action:function(e) {
e.preventDefault();
- selectsnapshot(uuid , name, "--generate" , "create",false,state) ;
+ selectsnapshot(uuid , name, "--generate" , "create",false,state,fstype) ;
}});
opts.push({text:_("Remove VM"), icon:"fa-minus", action:function(e) {
e.preventDefault();
@@ -200,7 +200,7 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
}
context.attach('#vm-'+uuid, opts);
}
-function addVMSnapContext(name, uuid, template, state, snapshotname, preview=false){
+function addVMSnapContext(name, uuid, template, state, snapshotname, method, preview=false){
var opts = [];
var path = location.pathname;
var x = path.indexOf("?");
@@ -213,7 +213,7 @@ function addVMSnapContext(name, uuid, template, state, snapshotname, preview=fal
e.preventDefault();
selectsnapshot(uuid, name, snapshotname, "revert",true) ;
}});
-
+ if (method == "QEMU") {
opts.push({text:_("Block Commit"), icon:"fa-hdd-o", action:function(e) {
$('#vm-'+uuid).find('i').removeClass('fa-play fa-square fa-pause').addClass('fa-refresh fa-spin');
e.preventDefault();
@@ -230,6 +230,7 @@ function addVMSnapContext(name, uuid, template, state, snapshotname, preview=fal
e.preventDefault();
ajaxVMDispatch({action:"domain-stop", uuid:uuid}, "loadlist");
}}); }
+ }
} else {
opts.push({text:_("Revert snapshot"), icon:"fa-fast-backward", action:function(e) {
e.preventDefault();
diff --git a/emhttp/plugins/dynamix/include/Helpers.php b/emhttp/plugins/dynamix/include/Helpers.php
index 0d2ecc3d5..2b9569c68 100644
--- a/emhttp/plugins/dynamix/include/Helpers.php
+++ b/emhttp/plugins/dynamix/include/Helpers.php
@@ -270,4 +270,25 @@ function my_preg_split($split, $text, $count=2) {
function delete_file(...$file) {
array_map('unlink',array_filter($file,'file_exists'));
}
+function my_mkdir($dirname,$permissions = 0777,$recursive = false) {
+ $dirname = transpose_user_path($dirname);
+ $pathinfo = pathinfo($dirname);
+ $parent = $pathinfo["dirname"];
+ $fstype = trim(shell_exec(" stat -f -c '%T' $parent"));
+ $rtncode = false;
+ switch ($fstype) {
+ case "zfs":
+ $zfsdataset = trim(shell_exec("zfs list -H -o name $parent")) ;
+ $rtncode=exec("zfs create $zfsdataset/{$pathinfo['filename']}");
+ if (!$rtncode) mkdir($dirname, $permissions, $recursive);
+ break;
+ case "btrfs":
+ $rtncode=exec("btrfs subvolume create $dirname");
+ if (!$rtncode) mkdir($dirname, $permissions, $recursive);
+ break;
+ default:
+ mkdir($dirname, $permissions, $recursive);
+ break;
+ }
+}
?>