diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page
index dceeed474..38f948558 100644
--- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page
+++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page
@@ -18,6 +18,7 @@ Markdown="false"
*/
?>
+$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
$cpus = cpu_list();
@@ -121,6 +122,7 @@ div.four label:nth-child(4n+4){float:none;clear:both}
div.four label.cpu1{width:32%}
div.four label.cpu2{width:26%}
div.template,div#dialogWindow,input#upload{display:none}
+div.template,div#dialogWindow2,input#upload{display:none}
table.domdisk thead tr th:nth-child(1){width:56%!important}
table.domdisk thead tr th:nth-child(n+2){width:8%!important}
table.domdisk thead tr th:nth-child(1){padding-left:72px}
@@ -134,7 +136,7 @@ i.mover{margin-right:8px;display:none}
.dropdown-menu{z-index:10001}
-_(Name)_ | _(Description)_ | _(CPUs)_ | _(Memory)_ | _(vDisks)_ | _(Graphics)_ | _(Autostart)_ |
+_(Name)_ | _(Description)_ | _(CPUs)_ | _(Memory)_ | _(vDisks / vCDs)_ | _(Graphics)_ | _(Autostart)_ |
|
@@ -163,7 +165,54 @@ function resetSorting() {
function changemedia(uuid,dev,bus,file) {
if (file === "--select") getisoimage(uuid,dev,bus,file);
if (file === "--eject") ajaxVMDispatch({action:"change-media", uuid:uuid , cdrom:"" , dev:dev , bus:bus , file:file}, "loadlist");
+}
+function getisoimageboth(uuid,dev,bus,file,dev2,bus2,file2){
+ var root = = '"'.$domain_cfg["MEDIADIR"].'"';?>;
+ var match= ".iso";
+ var box = $("#dialogWindow");
+ box.html($("#templateISOboth").html());
+
+ box.find('#target').attr('data-pickroot',root).attr('data-picktop',root).attr('data-pickmatch',match).attr('value', file).fileTreeAttach(null,null,function(path){
+ var bits = path.substr(1).split('/');
+ var auto = bits.length>3 ? '' : share;
+ box.find('#target').val(path+auto).change();
+ });
+ box.find('#target2').attr('data-pickroot',root).attr('data-picktop',root).attr('data-pickmatch',match).attr('value', file2).fileTreeAttach(null,null,function(path){
+ var bits = path.substr(1).split('/');
+ var auto = bits.length>3 ? '' : share;
+ box.find('#target2').val(path+auto).change();
+ });
+ var height = 100;
+ box.dialog({
+ title: "Select ISOs for CDROMs",
+ resizable: false,
+ width: 600,
+ height: 300,
+ modal: true,
+ show: {effect:'fade', duration:250},
+ hide: {effect:'fade', duration:250},
+ buttons: {
+ "_(Update)_": function(){
+ var target = box.find('#target');
+ if (target.length) {
+ target = target.val();
+ } else target = '';
+ var target2 = box.find('#target2');
+ if (target2.length) {
+ target2 = target2.val();
+ } else target2 = '';
+ box.find('#target').prop('disabled',true);
+ box.find('#target2').prop('disabled',true);
+ ajaxVMDispatch({action:"change-media-both", uuid:uuid , cdrom:"" , dev:dev , bus:bus , file:target, dev2:dev2 , bus2:bus2 , file2:target2}, "loadlist");
+ box.dialog('close');
+ },
+ "_(Cancel)_": function(){
+ box.dialog('close');
+ }
+ }
+ });
+ dialogStyle();
}
function getisoimage(uuid,dev,bus,file){
var root = = '"'.$domain_cfg["MEDIADIR"].'"';?>;
@@ -203,8 +252,171 @@ function getisoimage(uuid,dev,bus,file){
});
dialogStyle();
}
+
+function selectsnapshot(uuid, name ,snaps, opt, getlist,state){
+
+ var root = = '"'.$domain_cfg["MEDIADIR"].'"';?>;
+ var match= ".iso";
+ var box = $("#dialogWindow2");
+ box.html($("#templatesnapshot"+opt).html());
+ var height = 200;
+ const Capopt = opt.charAt(0).toUpperCase() + opt.slice(1) ;
+ var optiontext = Capopt + " Snapshot" ;
+ box.find('#VMName').html(name) ;
+ box.find('#targetsnap').val(snaps) ;
+ box.find('#targetsnapl').html(snaps) ;
+ if (getlist) {
+ var only = 1 ;
+ if (opt == "remove") only = 0;
+ $.post("/plugins/dynamix.vm.manager/include/VMajax.php", {action:"snap-images", uuid:uuid , snapshotname:snaps, only:only}, function(data) {
+ if (data.html) {
+ box.find('#targetsnapimages').html(data.html) ;
+ }
+ },'json');
+ }
+
+ document.getElementById("targetsnaprmv").checked = true ;
+ document.getElementById("targetsnaprmvmeta").checked = true ;
+ document.getElementById("targetsnapkeep").checked = true ;
+ document.getElementById("targetsnapfspc").checked = true ;
+
+ box.dialog({
+ title: "_("+optiontext+ ")_",
+ resizable: false,
+ width: 600,
+ height: 500,
+ modal: true,
+ show: {effect:'fade', duration:250},
+ hide: {effect:'fade', duration:250},
+
+ buttons: {
+ "_(Proceed)_": function(){
+ var target = box.find('#targetsnap');
+ if (target.length) {
+ target = target.val();
+ if (!target ) {errorTarget(); return;}
+ } else target = '';
+ var remove = 'yes'
+ var keep = 'yes'
+ var removemeta = 'yes'
+ var free = 'yes'
+ var desc = ''
+ box.find('#targetsnap').prop('disabled',true);
+ if (opt == "revert") {
+ var x = box.find('#targetsnaprmv').prop('checked') ;
+ if (x) remove = 'yes' ; else remove = 'no' ;
+ x = box.find('#targetsnaprmvmeta').prop('checked') ;
+ if (x) removemeta = 'yes' ; else removemeta = 'no' ;
+ x = box.find('#targetsnapkeep').prop('checked') ;
+ if (x) keep = 'yes' ; else keep = 'no' ;
+ }
+ if (opt == "create") {
+ var x = box.find('#targetsnapfspc').prop('checked') ;
+ if (x) free = 'yes' ; else free = 'no' ;
+ var 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");
+ box.dialog('close');
+ },
+ "_(Cancel)_": function(){
+ box.dialog('close');
+ }
+ }
+ });
+ dialogStyle();
+}
+
+function selectblock(uuid, name ,snaps, opt, getlist,state){
+
+ var root = = '"'.$domain_cfg["MEDIADIR"].'"';?>;
+ var match= ".iso";
+ var box = $("#dialogWindow2");
+ box.html($("#templateblock").html());
+ var height = 200;
+ const Capopt = opt.charAt(0).toUpperCase() + opt.slice(1) ;
+ var optiontext = Capopt + " Block Devices" ;
+ box.find('#VMName').html(name) ;
+ box.find('#targetsnap').val(snaps) ;
+ box.find('#targetsnapl').html(snaps) ;
+
+ getlist = true ;
+ if (getlist) {
+ var only = 1 ;
+ if (opt == "remove") only = 0;
+ $.post("/plugins/dynamix.vm.manager/include/VMajax.php", {action:"snap-list", uuid:uuid }, function(data) {
+ if (data.html) {
+ var targetbase = document.getElementById("targetblockbase") ;
+ htmlstrbase = ""
+ htmlstrtop = ""
+ $("select.targetblockbase").replaceWith(htmlstrbase) ;
+ $("select.targetblocktop").replaceWith(htmlstrtop) ;
+ }
+ },'json');
+ }
+
+ document.getElementById("targetsnaprmv").checked = true ;
+ document.getElementById("targetsnaprmvmeta").checked = true ;
+ document.getElementById("targetsnapkeep").checked = true ;
+ document.getElementById("targetsnapfspc").checked = true ;
+ if (opt== "pull") {
+ $('.toprow').hide();
+ $('.targetpivotrow').hide();
+ $('.targetdeleterow').hide();
+ } else {
+ $('.toprow').show();
+ $('.targetpivotrow').show();
+ $('.targetdeleterow').show();
+ }
+
+ box.dialog({
+ title: "_("+optiontext+ ")_",
+ resizable: false,
+ width: 600,
+ height: 500,
+ modal: true,
+ show: {effect:'fade', duration:250},
+ hide: {effect:'fade', duration:250},
+ buttons: {
+ _("Action")_: function(){
+ var target = box.find('#targetsnap');
+ if (target.length) {
+ target = target.val();
+ if (!target ) {errorTarget(); return;}
+ } else target = '';
+ var remove = 'yes'
+ var keep = 'yes'
+ var removemeta = 'yes'
+ var free = 'yes'
+ var delete_file = 'yes'
+ var pivot = 'yes'
+ var desc = ''
+ box.find('#targetsnap').prop('disabled',true);
+ if (opt == "create") {
+ var x = box.find('#targetsnapfspc').prop('checked') ;
+ if (x) free = 'yes' ; else free = 'no' ;
+ var desc = box.find("#targetsnapdesc").prop('value') ;
+ }
+ var targetbase = box.find("#targetblockbase").prop('value') ;
+ var targettop = box.find("#targetblocktop").prop('value') ;
+
+ x = box.find('#targetpivot').prop('checked') ;
+ if (x) pivot = 'yes' ; else pivot = 'no' ;
+ x = box.find('#targetdelete').prop('checked') ;
+ if (x) delete_file = 'yes' ; else delete_file = 'no' ;
+ Ajaxurl = "VMAjaxCall.php " + encodeURIComponent("/usr/local/emhttp/plugins/dynamix.vm.manager/include/VMajax.php&" + $.param({action:opt , name:name ,targetbase:targetbase, targettop:targettop , snapshotname:target , remove:remove, targetpivot:pivot ,removemeta:removemeta ,targetdelete:delete_file})) ;
+ openVMAction((Ajaxurl),"Block Commit", "dynamix.vm.manager", "loadlist") ;
+ box.dialog('close');
+ },
+ "_(Cancel)_": function(){
+ box.dialog('close');
+ }
+ }
+ });
+ dialogStyle();
+}
+
function dialogStyle() {
- $('.ui-dialog-titlebar-close').css({'background':'transparent','border':'none','font-size':'1.8rem','margin-top':'-14px','margin-right':'-18px'}).html('').prop('title',"_(Close)_").prop('onclick',null).off('click').click(function(){box.dialog('close');});
+ $('.ui-dialog-titlebar-close').css({'background':'transparent','border':'none','font-size':'1.8rem','margin-top':'-14px','margin-right':'-18px'}).html('').prop('title');
$('.ui-dialog-title').css({'text-align':'center','width':'100%','font-size':'1.8rem'});
$('.ui-dialog-content').css({'padding-top':'15px','vertical-align':'bottom'});
$('.ui-button-text').css({'padding':'0px 5px'});
@@ -304,5 +516,81 @@ $(function() {
_(ISO Image)_:
:
+
+
+_(CD1 ISO Image)_:
+:
+_(CD2 ISO Image)_:
+:
+
+
+
+
+
+
+_(VM Name)_:
+
+
+_(Snapshot Name)_:
+
+
+_(Remove Images)_:
+
+_(Remove Meta)_:
+
+
+
+
+
+
+
+
+
+_(!! Warning removing Snapshots can break the chain !!)_
+
+_(VM Name)_:
+
+
+_(Snapshot Name)_:
+
+
+
+
+
+
+_(VM Name)_:
+
+
+_(Snapshot Name)_:
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
index 7e103c663..c79cbdba8 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
@@ -21,6 +21,7 @@
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
$user_prefs = '/boot/config/plugins/dynamix.vm.manager/userprefs.cfg';
+if (file_exists('/boot/config/plugins/dynamix.vm.manager/vmpreview')) $vmpreview = true ; else $vmpreview = false ;
$vms = $lv->get_domains();
if (empty($vms)) {
echo ''._('No Virtual Machines installed').' |
';
@@ -51,6 +52,8 @@
$icon = $lv->domain_get_icon_url($res);
$image = substr($icon,-4)=='.png' ? "" : (substr($icon,0,5)=='icon-' ? "" : "");
$arrConfig = domain_to_config($uuid);
+ $snapshots = getvmsnapshots($vm) ;
+ $cdroms = $lv->get_cdrom_stats($res) ;
if ($state == 'running') {
$mem = $dom['memory']/1024;
} else {
@@ -101,7 +104,7 @@
}
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')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log), $vmrcconsole);
+ $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);
$kvm[] = "kvm.push({id:'$uuid',state:'$state'});";
switch ($state) {
case 'running':
@@ -123,12 +126,33 @@
}
/* VM information */
+
+ if ($snapshots != null) $snapshotstr = _("(Snapshots :").count($snapshots).')' ; else $snapshotstr = _("(Snapshots :None)") ;
+ $cdbus = $cdbus2 = $cdfile = $cdfile2 = "" ;
+ $cdromcount = 0 ;
+ foreach ($cdroms as $arrCD) {
+ $disk = $arrCD['file'] ?? $arrCD['partition'];
+ $dev = $arrCD['device'];
+ $bus = $arrValidDiskBuses[$arrCD['bus']] ?? 'VirtIO';
+ if ($dev == "hda") {
+ $cdbus = $arrValidDiskBuses[$arrCD['bus']] ?? 'VirtIO';
+ $cdfile = $arrCD['file'] ?? $arrCD['partition'];
+ if ($cdfile != "") $cdromcount++ ;
+ }
+ if ($dev == "hdb") {
+ $cdbus2 = $arrValidDiskBuses[$arrCD['bus']] ?? 'VirtIO';
+ $cdfile2 = $arrCD['file'] ?? $arrCD['partition'];
+ if ($cdfile2 != "") $cdromcount++ ;
+ }
+ }
+ $changemedia = "getisoimageboth(\"{$uuid}\",\"hda\",\"{$cdbus}\",\"{$cdfile}\",\"hdb\",\"{$cdbus2}\",\"{$cdfile2}\")";
+ $cdstr = $cdromcount." / 2 ";
echo "";
- echo "$image$vm "._($status)." | ";
+ echo "$image$vm
"._($status)." $snapshotstcount";
echo "$desc | ";
echo "$vcpu | ";
echo "$mem | ";
- echo "$disks | ";
+ echo "$disks $cdstr $snapshotstr | ";
echo "$graphics | ";
echo " |
";
@@ -168,7 +192,7 @@
}
/* Display VM cdroms */
- foreach ($lv->get_cdrom_stats($res) as $arrCD) {
+ foreach ($cdroms as $arrCD) {
$capacity = $lv->format_size($arrCD['capacity'], 0);
$allocation = $lv->format_size($arrCD['allocation'], 0);
$disk = $arrCD['file'] ?? $arrCD['partition'];
@@ -218,8 +242,38 @@
if ($gastate == "disconnected") echo ""._('Guest agent not installed')." | | | |
";
else echo ""._('Guest not running')." | | | | |
";
}
- echo "";
- echo "";
+ echo "";
+ /* Display VM Snapshots */
+ if ($snapshots != null) {
+ $j=0 ;
+ $steps = array() ;
+ foreach($snapshots as $snap) {
+ if ($snap['parent'] == "" || $snap['parent'] == "Base") $j++;
+ $steps[$j] .= $snap['name'].';' ;
+ }
+ echo " "._('Snapshots')." | | "._('Date/Time')." | "._('Type')." | "._('Parent')." | "._('Memory')." |
";
+ echo "";
+ foreach($steps as $stepsline)
+ {
+ $snapshotlist = explode(";",$stepsline) ;
+ $tab = " " ;
+ foreach($snapshotlist as $snapshotitem) {
+ if ($snapshotitem == "") continue ;
+ $snapshot = $snapshots[$snapshotitem] ;
+ $snapshotstate = _(ucfirst($snapshot["state"])) ;
+ $snapshotdesc = $snapshot["desc"] ;
+ $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);
+ echo "$tab|__ ".$snapshot["name"]." | $snapshotdesc | $snapshotdatetime | $snapshotstate | $snapshotparent | $snapshotmemory |
";
+ $tab .=" " ;
+ }
+ echo "";
+ }
+ }
+ echo "";
}
+
echo "\0".implode($kvm);
?>
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
index 4ee16c5b9..ed28dffe0 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
@@ -121,6 +121,7 @@ function embed(&$syslinux, $key, $value) {
$vvarray[] = "type=$protocol\n";
$vvarray[] = "host="._var($_SERVER,'HTTP_HOST')."\n" ;
$vvarray[] = "port=$port\n" ;
+ $vvarray[] = "delete-this-file=1\n" ;
if (!is_dir("/mnt/user/system/remoteviewer")) mkdir("/mnt/user/system/remoteviewer") ;
$vvfile = "/mnt/user/system/remoteviewer/rv"._var($_SERVER,'HTTP_HOST').".$port.vv" ;
file_put_contents($vvfile,$vvarray) ;
@@ -139,6 +140,7 @@ function embed(&$syslinux, $key, $value) {
$vvarray[] = "type=$protocol\n";
$vvarray[] = "host="._var($_SERVER,'HTTP_HOST')."\n" ;
$vvarray[] = "port=$port\n" ;
+ $vvarray[] = "delete-this-file=1\n" ;
if (!is_dir("/mnt/user/system/remoteviewer")) mkdir("/mnt/user/system/remoteviewer") ;
$vvfile = "/mnt/user/system/remoteviewer/rv"._var($_SERVER,'HTTP_HOST').".$port.vv" ;
file_put_contents($vvfile,$vvarray) ;
@@ -263,6 +265,40 @@ function embed(&$syslinux, $key, $value) {
: ['error' => "Change Media Failed"];
break;
+case 'change-media-both':
+ requireLibvirt();
+ $res = $lv->get_domain_by_name($domName);
+ $cdroms = $lv->get_cdrom_stats($res) ;
+ $hda = $hdb = false ;
+ foreach ($lv->get_cdrom_stats($res) as $cd){
+ if ($cd['device'] == 'hda') $hda = true ;
+ if ($cd['device'] == 'hdb') $hdb = true ;
+ }
+ $file= $_REQUEST['file'];
+ if ($file != "" && $hda == false) {
+ $cmdstr = "virsh attach-disk '$domName' '$file' hda --type cdrom --targetbus sata --config" ;
+ } else {
+ if ($file == "") $cmdstr = "virsh change-media '$domName' hda --eject --current";
+ else $cmdstr = "virsh change-media '$domName' hda '$file'";
+ }
+ $rtn=shell_exec($cmdstr)
+ ? ['success' => true]
+ : ['error' => "Change Media Failed"];
+
+ if (isset($rtn['error'])) return ;
+
+ $file2 = $_REQUEST['file2'];
+ if ($file2 != "" && $hdb == false) {
+ $cmdstr = "virsh attach-disk '$domName' '$file2' hdb --type cdrom --targetbus sata --config" ;
+ } else {
+ if ($file2 == "") $cmdstr = "virsh change-media '$domName' hdb --eject --current";
+ else $cmdstr = "virsh change-media '$domName' hdb '$file2' ";
+ }
+ $rtn=shell_exec($cmdstr)
+ ? ['success' => true]
+ : ['error' => "Change Media Failed"];
+ break;
+
case 'memory-change':
requireLibvirt();
$arrResponse = $lv->domain_set_memory($domName, $_REQUEST['memory']*1024)
@@ -300,6 +336,40 @@ function embed(&$syslinux, $key, $value) {
: ['error' => $lv->get_last_error()];
break;
+case 'snap-create-external':
+ requireLibvirt();
+ $arrResponse = vm_snapshot($domName,$_REQUEST['snapshotname'],$_REQUEST['desc'],$_REQUEST['free']) ;
+ break;
+
+case 'snap-images':
+ requireLibvirt();
+ $html = vm_snapimages($domName,$_REQUEST['snapshotname'],$_REQUEST['only']) ;
+ $arrResponse = ['html' => $html , 'success' => true] ;
+ break;
+
+case 'snap-list':
+ requireLibvirt();
+ $arrResponse = ($data = getvmsnapshots($domName))
+ ? ['success' => true]
+ : ['error' => $lv->get_last_error()];
+ $datartn = "";
+ foreach($data as $snap=>$snapdetail) {
+ $snapshotdatetime = date("Y-m-d H:i:s",$snapdetail["creationtime"]) ;
+ $datartn .= "" ;
+ }
+ $arrResponse['html'] = $datartn ;
+ break;
+
+case 'snap-revert-external':
+ requireLibvirt();
+ $arrResponse = vm_revert($domName,$_REQUEST['snapshotname'],$_REQUEST['remove'], $_REQUEST['removemeta']) ;
+ break;
+
+case 'snap-remove-external':
+ requireLibvirt();
+ $arrResponse = vm_snapremove($domName,$_REQUEST['snapshotname']) ;
+ break;
+
case 'snap-delete':
requireLibvirt();
$arrResponse = $lv->domain_snapshot_delete($domName, $_REQUEST['snap'])
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
index 7652564ad..4587b50d2 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
@@ -770,7 +770,7 @@ function config_to_xml($config) {
$vmrc = "
-
+
$vmrcmousemode
@@ -1446,7 +1446,7 @@ function domain_get_icon_url($domain) {
function domain_change_xml($domain, $xml) {
$dom = $this->get_domain_object($domain);
- if (!($old_xml = domain_get_xml($dom)))
+ if (!($old_xml = $this->domain_get_xml($dom)))
return $this->_set_last_error();
if (!libvirt_domain_undefine($dom))
return $this->_set_last_error();
@@ -1846,6 +1846,48 @@ function nvram_restore($uuid) {
return false;
}
+ function nvram_create_snapshot($uuid,$snapshotname) {
+ // snapshot backup OVMF VARS if this domain had them
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi.fd')) {
+ copy('/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi.fd', '/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd');
+ return true;
+ }
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi-tpm.fd')) {
+ copy('/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi-tpm.fd', '/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd');
+ return true;
+ }
+ return false;
+ }
+
+ function nvram_revert_snapshot($uuid,$snapshotname) {
+ // snapshot backup OVMF VARS if this domain had them
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd')) {
+ copy('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd', '/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi.fd');
+ unlink('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd') ;
+ return true;
+ }
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd')) {
+ copy('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd', '/etc/libvirt/qemu/nvram/'.$uuid.'_VARS-pure-efi-tpm.fd');
+ unlink('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd') ;
+ return true;
+ }
+ return false;
+ }
+
+ function nvram_delete_snapshot($uuid,$snapshotname) {
+ // snapshot backup OVMF VARS if this domain had them
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd')) {
+ unlink('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi.fd') ;
+ return true;
+ }
+ if (is_file('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd')) {
+ unlink('/etc/libvirt/qemu/nvram/'.$uuid.$snapshotname.'_VARS-pure-efi-tpm.fd') ;
+ return true;
+ }
+ return false;
+ }
+
+
function is_dir_empty($dir) {
if (!is_readable($dir)) return NULL;
$handle = opendir($dir);
@@ -2461,6 +2503,12 @@ function domain_snapshots_list($domain) {
return ($tmp) ? $tmp : $this->_set_last_error();
}
+ //list all snapshots for domain
+ function domain_snapshot_get_xml($domain) {
+ $tmp = libvirt_domain_snapshot_get_xml($domain);
+ return ($tmp) ? $tmp : $this->_set_last_error();
+ }
+
// create a snapshot and metadata node for description
function domain_snapshot_create($domain) {
$this->domain_set_metadata($domain);
@@ -2470,10 +2518,9 @@ function domain_snapshot_create($domain) {
}
//delete snapshot and metadata
- function domain_snapshot_delete($domain, $name) {
- $this->snapshot_remove_metadata($domain, $name);
+ function domain_snapshot_delete($domain, $name, $flags=0) {
$name = $this->domain_snapshot_lookup_by_name($domain, $name);
- $tmp = libvirt_domain_snapshot_delete($name);
+ $tmp = libvirt_domain_snapshot_delete($name,$flags);
return ($tmp) ? $tmp : $this->_set_last_error();
}
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
index 5d9193c3e..bd1be61c4 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
@@ -163,6 +163,7 @@ private static function isValidTagName($tag){
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt.php";
+ require_once "$docroot/webGui/include/Custom.php";
// Load emhttp variables if needed.
if (!isset($var)){
@@ -1204,6 +1205,7 @@ function domain_to_config($uuid) {
$arrDisks[] = [
'new' => $strPath,
'size' => '',
+ 'driver' => $disk['type'],
'driver' => 'raw',
'dev' => $disk['device'],
'bus' => $disk['bus'],
@@ -1455,4 +1457,648 @@ function getcopypaste($res) {
if ($spicevmc || $qemuvdaagent) $copypaste = true ; else $copypaste = false ;
return $copypaste ;
}
+
+ function compare_creationtime($a, $b) {
+ return strnatcmp($a['creationtime'], $b['creationtime']);
+ }
+
+ function compare_creationtimelt($a, $b) {
+ return $a['creationtime'] < $b['creationtime'];
+ }
+
+ function getvmsnapshots($vm) {
+ $snaps=array() ;
+ $dbpath = "/etc/libvirt/qemu/snapshot/$vm" ;
+ $snaps_json = file_get_contents($dbpath."/snapshots.db") ;
+ $snaps = json_decode($snaps_json,true) ;
+ if (is_array($snaps)) uasort($snaps,'compare_creationtime') ;
+ return $snaps ;
+ }
+
+ function write_snapshots_database($vm,$name) {
+ global $lv ;
+ $dbpath = "/etc/libvirt/qemu/snapshot/$vm" ;
+ if (!is_dir($dbpath)) mkdir($dbpath) ;
+ $snaps_json = file_get_contents($dbpath."/snapshots.db") ;
+ $snaps = json_decode($snaps_json,true) ;
+ $snapshot_res=$lv->domain_snapshot_lookup_by_name($vm,$name) ;
+ $snapshot_xml=$lv->domain_snapshot_get_xml($snapshot_res) ;
+ $a = simplexml_load_string($snapshot_xml) ;
+ $a = json_encode($a) ;
+ $b= json_decode($a, TRUE);
+ $vmsnap = $b["name"] ;
+ $snaps[$vmsnap]["name"]= $b["name"];
+ $snaps[$vmsnap]["parent"]= $b["parent"] ;
+ $snaps[$vmsnap]["state"]= $b["state"];
+ $snaps[$vmsnap]["desc"]= $b["description"];
+ $snaps[$vmsnap]["memory"]= $b["memory"];
+ $snaps[$vmsnap]["creationtime"]= $b["creationTime"];
+
+ $disks =$lv->get_disk_stats($vm) ;
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $output = "" ;
+ exec("qemu-img info --backing-chain -U '$file' | grep image:",$output) ;
+ foreach($output as $key => $line) {
+ $line=str_replace("image: ","",$line) ;
+ $output[$key] = $line ;
+ }
+
+ $snaps[$vmsnap]['backing'][$disk["device"]] = $output ;
+ $rev = "r".$disk["device"] ;
+ $reversed = array_reverse($output) ;
+ $snaps[$vmsnap]['backing'][$rev] = $reversed ;
+ }
+ $parentfind = $snaps[$vmsnap]['backing'][$disk["device"]] ;
+ $parendfileinfo = pathinfo($parentfind[1]) ;
+ $snaps[$vmsnap]["parent"]= $parendfileinfo["extension"];
+ $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"];
+
+
+ $value = json_encode($snaps,JSON_PRETTY_PRINT) ;
+ file_put_contents($dbpath."/snapshots.db",$value) ;
+ }
+
+ function refresh_snapshots_database($vm) {
+ global $lv ;
+ $dbpath = "/etc/libvirt/qemu/snapshot/$vm" ;
+ if (!is_dir($dbpath)) mkdir($dbpath) ;
+ $snaps_json = file_get_contents($dbpath."/snapshots.db") ;
+ $snaps = json_decode($snaps_json,true) ;
+ foreach($snaps as $vmsnap=>$snap)
+
+ $disks =$lv->get_disk_stats($vm) ;
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $output = "" ;
+ exec("qemu-img info --backing-chain -U '$file' | grep image:",$output) ;
+ foreach($output as $key => $line) {
+ $line=str_replace("image: ","",$line) ;
+ $output[$key] = $line ;
+ }
+
+ $snaps[$vmsnap]['backing'][$disk["device"]] = $output ;
+ $rev = "r".$disk["device"] ;
+ $reversed = array_reverse($output) ;
+ $snaps[$vmsnap]['backing'][$rev] = $reversed ;
+ }
+ $parentfind = $snaps[$vmsnap]['backing'][$disk["device"]] ;
+ $parendfileinfo = pathinfo($parentfind[1]) ;
+ $snaps[$vmsnap]["parent"]= $parendfileinfo["extension"];
+ $snaps[$vmsnap]["parent"] = str_replace("qcow2",'',$snaps[$vmsnap]["parent"]) ;
+ if (isset($parentfind[1]) && !isset($parentfind[2])) $snaps[$vmsnap]["parent"]="Base" ;
+
+ $value = json_encode($snaps,JSON_PRETTY_PRINT) ;
+ $res = $lv->get_domain_by_name($vm);
+ if (!empty($lv->domain_get_ovmf($res))) $nvram = $lv->nvram_create_snapshot($lv->domain_get_uuid($vm),$name) ;
+
+
+ #Remove any NVRAMs that are no longer valid.
+ # Get uuid
+ $vmuuid = $lv->domain_get_uuid($vm) ;
+ #Get list of files
+ #$filepath = "/etc/libvirt/qemu/nvram/'.$uuid*" ; #$snapshotname"
+ $filepath = "/etc/libvirt/qemu/nvram/$vmuuid*" ; #$snapshotname"
+ $nvram_files=glob($filepath) ;
+ foreach($nvram_files as $key => $nvram_file) {
+ if ($nvram_file == "/etc/libvirt/qemu/nvram/$vmuuid"."_VARS-pure-efi.fd" || $nvram_file == "/etc/libvirt/qemu/nvram/$vmuuid"."_VARS-pure-efi-tpm.fd" ) unset($nvram_files[$key]) ;
+ foreach ($snaps as $snapshotname => $snap) {
+ $tpmfilename = "/etc/libvirt/qemu/nvram/".$vmuuid.$snapshotname."_VARS-pure-efi-tpm.fd" ;
+ $nontpmfilename = "/etc/libvirt/qemu/nvram/".$vmuuid.$snapshotname."_VARS-pure-efi.fd" ;
+ if ($nvram_file == $tpmfilename || $nvram_file == $nontpmfilename ) {
+ unset($nvram_files[$key]) ;}
+ }
+ }
+ foreach ($nvram_files as $nvram_file) unlink($nvram_file) ;
+
+
+ file_put_contents($dbpath."/snapshots.db",$value) ;
+ }
+
+ function delete_snapshots_database($vm,$name) {
+ global $lv ;
+ $dbpath = "/etc/libvirt/qemu/snapshot/$vm" ;
+ $snaps_json = file_get_contents($dbpath."/snapshots.db") ;
+ $snaps = json_decode($snaps_json,true) ;
+ unset($snaps[$name]) ;
+ $value = json_encode($snaps,JSON_PRETTY_PRINT) ;
+ file_put_contents($dbpath."/snapshots.db",$value) ;
+ return true ;
+ }
+
+
+ function vm_snapshot($vm,$snapshotname, $snapshotdesc, $free = "yes", $memorysnap = "yes") {
+ global $lv ;
+
+ #Get State
+ $res = $lv->get_domain_by_name($vm);
+ $dom = $lv->domain_get_info($res);
+ $state = $lv->domain_state_translate($dom['state']);
+
+ #Get disks for --diskspec
+ $disks =$lv->get_disk_stats($vm) ;
+ $diskspec = "" ;
+ $capacity = 0 ;
+ if ($snapshotname == "--generate") $name= "S" . date("YmdHis") ; else $name=$snapshotname ;
+ if ($snapshotdesc != "") $snapshotdesc = " --description '$snapshotdesc'" ;
+
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $pathinfo = pathinfo($file) ;
+ $filenew = $pathinfo["dirname"].'/'.$pathinfo["filename"].'.'.$name.'qcow2' ;
+ $diskspec .= " --diskspec '".$disk["device"]."',snapshot=external,file='".$filenew."'" ;
+ $capacity = $capacity + $disk["capacity"] ;
+ }
+ $dirpath = $pathinfo["dirname"] ;
+ #get memory
+ $mem = $lv->domain_get_memory_stats($vm) ;
+ $memory = $mem[6] ;
+
+ if ($memorysnap = "yes") $memspec = " --memspec ".$pathinfo["dirname"].'/memory"'.$name.'".mem,snapshot=external' ; else $memspec = "" ;
+ $cmdstr = "virsh snapshot-create-as '$vm' --name '$name' $snapshotdesc --atomic" ;
+
+
+ if ($state == "running") {
+ $cmdstr .= " --live ".$memspec.$diskspec ;
+ $capacity = $capacity + $memory ;
+
+ } else {
+ $cmdstr .= " --disk-only ".$diskspec ;
+ }
+
+ #Check free space.
+ $dirfree = disk_free_space($pathinfo["dirname"]) ;
+
+ $capacity *= 1 ;
+
+ if ($free == "yes" && $dirfree < $capacity) { $arrResponse = ['error' => _("Insufficent Storage for Snapshot")]; return $arrResponse ;}
+
+ #Copy nvram
+ if (!empty($lv->domain_get_ovmf($res))) $nvram = $lv->nvram_create_snapshot($lv->domain_get_uuid($vm),$name) ;
+
+ $xmlfile = $pathinfo["dirname"].'/"'.$name.'".running' ;
+ if ($state == "running") exec("virsh dumpxml '$vm' > $xmlfile",$outxml,$rtnxml) ;
+
+ $test = false ;
+ if ($test) exec($cmdstr." --print-xml 2>&1",$output,$return) ; else exec($cmdstr." 2>&1",$output,$return) ;
+
+ if (strpos(" ".$output[0],"error") ) {
+ $arrResponse = ['error' => substr($output[0],6) ] ;
+ } else {
+ # Remove nvram snapshot
+ $arrResponse = ['success' => true] ;
+ }
+ write_snapshots_database("$vm","$name") ;
+ #remove meta data
+ $ret = $lv->domain_snapshot_delete($vm, "$name" ,2) ;
+ return $arrResponse ;
+
+ }
+
+ function vm_revert($vm, $snap="--current",$action="no",$actionmeta = 'yes') {
+ global $lv ;
+ $snapslist= getvmsnapshots($vm) ;
+ $disks =$lv->get_disk_stats($vm) ;
+
+ switch ($snapslist[$snap]['state']) {
+ case "shutoff":
+ case "running":
+ #VM must be shutdown.
+ $res = $lv->get_domain_by_name($vm);
+ $dom = $lv->domain_get_info($res);
+ $state = $lv->domain_state_translate($dom['state']);
+ # if VM running shutdown. Record was running.
+ if ($state != 'shutdown') $arrResponse = $lv->domain_destroy($vm) ;
+ # Wait for shutdown?
+ # GetXML
+ $strXML= $lv->domain_get_xml($res) ;
+ $xmlobj = custom::createArray('domain',$strXML) ;
+
+ # Process disks and update path.
+ $disks=($snapslist[$snap]['disks']) ;
+ foreach ($disks as $disk) {
+ $diskname = $disk["@attributes"]["name"] ;
+ if ($diskname == "hda" || $diskname == "hdb") continue ;
+ $path = $disk["source"]["@attributes"]["file"] ;
+ if ($diskname == "hdc") {
+ $primarypathinfo = pathinfo($path) ;
+ $primarypath = $primarypathinfo['dirname'] ;
+ }
+ $item = array_search($path,$snapslist[$snap]['backing'][$diskname]) ;
+ $newpath = $snapslist[$snap]['backing'][$diskname][$item + 1];
+ $json_info = getDiskImageInfo($newpath) ;
+ foreach($xmlobj['devices']['disk'] as $ddk => $dd){
+ if ($dd['target']["@attributes"]['dev'] == $diskname) {
+ $xmlobj['devices']['disk'][$ddk]['source']["@attributes"]['file'] = "$newpath" ;
+ $xmlobj['devices']['disk'][$ddk]['driver']["@attributes"]['type'] = $json_info["format"] ;
+ }
+ }
+ }
+ $xml = custom::createXML('domain',$xmlobj)->saveXML();
+ $new = $lv->domain_define($xml);
+ if ($new)
+ $arrResponse = ['success' => true] ; else
+ $arrResponse = ['error' => $lv->get_last_error()] ;
+
+ # remove snapshot meta data and images for all snpahots.
+
+ foreach ($disks as $disk) {
+ $diskname = $disk["@attributes"]["name"] ;
+ if ($diskname == "hda" || $diskname == "hdb") continue ;
+ $path = $disk["source"]["@attributes"]["file"] ;
+ if (is_file($path) && $action == "yes") unlink("$path") ;
+ $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] ;
+ if (is_file($newpath) && $action == "yes") unlink("$newpath") ;
+ $item++ ;
+ }
+ }
+
+ uasort($snapslist,'compare_creationtimelt') ;
+ foreach($snapslist as $s) {
+ $name = $s['name'] ;
+
+ $xmlfile = $primarypath."/$name.running" ;
+ $memoryfile = $primarypath."/memory$name.mem" ;
+
+ if ($snapslist[$snap]['state'] == "running") {
+ # Set XML to saved XML
+ $xml = file_get_contents($xmlfile) ;
+ $xmlobj = custom::createArray('domain',$xml) ;
+ $xml = custom::createXML('domain',$xmlobj)->saveXML();
+ $rtn = $lv->domain_define($xml) ;
+
+ # Resotre Memeory.
+
+ $makerun = true ;
+ if ($makerun == true) exec("virsh restore $memoryfile") ;
+ #exec("virsh restore $memoryfile") ;
+ }
+ #Delete Metadata only.
+ if ($actionmeta == "yes") {
+ $ret = delete_snapshots_database("$vm","$name") ;
+ }
+ if (is_file($memoryfile) && $action == "yes") unlink($memoryfile) ;
+ if (is_file($xmlfile) && $action == "yes") unlink($xmlfile) ;
+ if ($s['name'] == $snap) break ;
+ }
+ #if VM was started restart.
+ if ($state == 'running' && $snapslist[$snap]['state'] != "running") {
+ $arrResponse = $lv->domain_start($vm) ;
+ }
+
+ if (!empty($lv->domain_get_ovmf($res))) $nvram = $lv->nvram_revert_snapshot($lv->domain_get_uuid($vm),$name) ;
+ break ;
+
+ }
+ $arrResponse = ['success' => true] ;
+ return($arrResponse) ;
+ }
+
+ function vm_snapimages($vm, $snap, $only) {
+ global $lv ;
+ $snapslist= getvmsnapshots($vm) ;
+ $data = "
Images and metadata to remove if tickbox checked.
" ;
+
+ $disks =$lv->get_disk_stats($vm) ;
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $output = "" ;
+ exec("qemu-img info --backing-chain -U '$file' | grep image:",$output) ;
+ foreach($output as $key => $line) {
+ $line=str_replace("image: ","",$line) ;
+ $output[$key] = $line ;
+ }
+ $snaps[$vm][$disk["device"]] = $output ;
+ $rev = "r".$disk["device"] ;
+ $reversed = array_reverse($output) ;
+ $snaps[$vm][$rev] = $reversed ;
+ $pathinfo = pathinfo($file) ;
+ $filenew = $pathinfo["dirname"].'/'.$pathinfo["filename"].'.'.$name.'qcow2' ;
+ $diskspec .= " --diskspec ".$disk["device"].",snapshot=external,file=".$filenew ;
+ $capacity = $capacity + $disk["capacity"] ;
+ }
+
+ $snapdisks= $snapslist[$snap]['disks'] ;
+
+ foreach ($snapdisks as $diskkey => $snapdisk) {
+ $diskname = $snapdisk["@attributes"]["name"] ;
+ if ($diskname == "hda" || $diskname == "hdb") continue ;
+ $path = $snapdisk["source"]["@attributes"]["file"] ;
+ if (is_file($path)) $data .= "$path
" ;
+ $item = array_search($path,$snaps[$vm]["r".$diskname]) ;
+ $item++ ;
+ if ($only == 0) $item = 0 ;
+ while($item > 0)
+ {
+ if (!isset($snaps[$vm]["r".$diskname][$item])) break ;
+ $newpath = $snaps[$vm]["r".$diskname][$item] ;
+ if (is_file($path)) $data .= "$newpath
" ;
+ $item++ ;
+
+ }
+ }
+ $data .= "
Snapshots metadata to remove." ;
+ if ($only == 0) {
+ $data .= "
$snap";
+ } else {
+ uasort($snapslist,'compare_creationtimelt') ;
+ foreach($snapslist as $s) {
+ $name = $s['name'] ;
+ $data .= "
$name";
+ if ($s['name'] == $snap) break ;
+ }
+ }
+ return($data) ;
+ }
+
+
+ function vm_snapremove($vm, $snap) {
+ global $lv ;
+ $snapslist= getvmsnapshots($vm) ;
+ $res = $lv->get_domain_by_name($vm);
+ $dom = $lv->domain_get_info($res);
+
+ $disks =$lv->get_disk_stats($vm) ;
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $output = "" ;
+ exec("qemu-img info --backing-chain -U $file | grep image:",$output) ;
+ foreach($output as $key => $line) {
+ $line=str_replace("image: ","",$line) ;
+ $output[$key] = $line ;
+ }
+
+ $snaps[$vm][$disk["device"]] = $output ;
+ $rev = "r".$disk["device"] ;
+ $reversed = array_reverse($output) ;
+ $snaps[$vm][$rev] = $reversed ;
+ $pathinfo = pathinfo($file) ;
+ }
+
+ # GetXML
+ $strXML= $lv->domain_get_xml($res) ;
+ $xmlobj = custom::createArray('domain',$strXML) ;
+
+ # Process disks.
+ $disks=($snapslist[$snap]['disks']) ;
+ foreach ($disks as $disk) {
+ $diskname = $disk["@attributes"]["name"] ;
+ if ($diskname == "hda" || $diskname == "hdb") continue ;
+ $path = $disk["source"]["@attributes"]["file"] ;
+ $item = array_search($path,$snaps[$vm][$diskname]) ;
+ if ($item!==false) {
+ $data = ["error" => "Image currently active for this domain."] ;
+ return ($data) ;
+ }
+ }
+
+ $disks=($snapslist[$snap]['disks']) ;
+ foreach ($disks as $disk) {
+ $diskname = $disk["@attributes"]["name"] ;
+ if ($diskname == "hda" || $diskname == "hdb") continue ;
+ $path = $disk["source"]["@attributes"]["file"] ;
+ if (is_file($path)) {
+ if(!unlink("$path")) {
+ $data = ["error" => "Unable to remove image file $path"] ;
+ return ($data) ;
+ }
+ }
+ }
+
+ # Delete NVRAM
+ if (!empty($lv->domain_get_ovmf($res))) $nvram = $lv->nvram_delete_snapshot($lv->domain_get_uuid($vm),$snap) ;
+
+ $ret = delete_snapshots_database("$vm","$snap") ;
+
+ if(!$ret)
+ $data = ["error" => "Unable to remove snap metadata $snap"] ;
+ else
+ $data = ["success => 'true"] ;
+
+ return($data) ;
+ }
+
+function vm_blockcommit($vm, $snap ,$path,$base,$top,$pivot,$action) {
+ global $lv ;
+ /*
+ NAME
+ blockcommit - Start a block commit operation.
+
+ SYNOPSIS
+ blockcommit [--bandwidth ] [--base ] [--shallow] [--top ] [--active] [--delete] [--wait] [--verbose] [--timeout ] [--pivot] [--keep-overlay] [--async] [--keep-relative] [--bytes]
+
+ DESCRIPTION
+ Commit changes from a snapshot down to its backing image.
+
+ OPTIONS
+ [--domain] domain name, id or uuid
+ [--path] fully-qualified path of disk
+ --bandwidth bandwidth limit in MiB/s
+ --base path of base file to commit into (default bottom of chain)
+ --shallow use backing file of top as base
+ --top path of top file to commit from (default top of chain)
+ --active trigger two-stage active commit of top file
+ --delete delete files that were successfully committed
+ --wait wait for job to complete (with --active, wait for job to sync)
+ --verbose with --wait, display the progress
+ --timeout implies --wait, abort if copy exceeds timeout (in seconds)
+ --pivot implies --active --wait, pivot when commit is synced
+ --keep-overlay implies --active --wait, quit when commit is synced
+ --async with --wait, don't wait for cancel to finish
+ --keep-relative keep the backing chain relatively referenced
+ --bytes the bandwidth limit is in bytes/s rather than MiB/s
+
+ blockcommit Debian --path /mnt/user/domains/Debian/vdisk1.S20230513120410qcow2 --verbose --pivot --delete
+ */
+ # Error if VM Not running.
+
+ $snapslist= getvmsnapshots($vm) ;
+ $disks =$lv->get_disk_stats($vm) ;
+
+ foreach($disks as $disk) {
+ $path = $disk['file'] ;
+ $cmdstr = "virsh blockcommit '$vm' --path '$path' --verbose " ;
+ if ($pivot == "yes") $cmdstr .= " --pivot " ;
+ if ($action == "yes") $cmdstr .= " --delete " ;
+ # Process disks and update path.
+ $snapdisks=($snapslist[$snap]['disks']) ;
+ if ($base != "--base" && $base != "") {
+ #get file name from snapshot.
+ $snapdisks=($snapslist[$base]['disks']) ;
+ $basepath = "" ;
+ foreach ($snapdisks as $snapdisk) {
+ $diskname = $snapdisk["@attributes"]["name"] ;
+ if ($diskname != $disk['device']) continue ;
+ $basepath = $snapdisk["source"]["@attributes"]["file"] ;
+ }
+ if ($basepath != "") $cmdstr .= " --base '$basepath' ";
+ }
+ if ($top != "--top" && $top !="") {
+ #get file name from snapshot.
+ $snapdisks=($snapslist[$top]['disks']) ;
+ $toppath = "" ;
+ foreach ($snapdisks as $snapdisk) {
+ $diskname = $snapdisk["@attributes"]["name"] ;
+ if ($diskname != $disk['device']) continue ;
+ $toppath = $snapdisk["source"]["@attributes"]["file"] ;
+ }
+ if ($toppath != "") $cmdstr .= " --top '$toppath' ";
+ }
+
+ $error = execCommand_nchan($cmdstr,$path) ;
+ if (!$error) {
+ $arrResponse = ['error' => substr($output[0],6) ] ;
+ return($arrResponse) ;
+ } else {
+ $arrResponse = ['success' => true] ;
+ }
+
+ }
+ # Delete NVRAM
+ #if (!empty($lv->domain_get_ovmf($res))) $nvram = $lv->nvram_delete_snapshot($lv->domain_get_uuid($vm),$snap) ;
+
+ refresh_snapshots_database($vm) ;
+ $ret = $ret = delete_snapshots_database("$vm","$snap") ; ;
+ if($ret)
+ $data = ["error" => "Unable to remove snap metadata $snap"] ;
+ else
+ $data = ["success => 'true"] ;
+ return $data ;
+
+}
+
+function vm_blockpull($vm, $snap ,$path,$base,$top,$pivot,$action) {
+ global $lv ;
+ /*
+ NAME
+ blockpull - Populate a disk from its backing image.
+
+ SYNOPSIS
+ blockpull [--bandwidth ] [--base ] [--wait] [--verbose] [--timeout ] [--async] [--keep-relative] [--bytes]
+
+ DESCRIPTION
+ Populate a disk from its backing image.
+
+ OPTIONS
+ [--domain] domain name, id or uuid
+ [--path] fully-qualified path of disk
+ --bandwidth bandwidth limit in MiB/s
+ --base path of backing file in chain for a partial pull
+ --wait wait for job to finish
+ --verbose with --wait, display the progress
+ --timeout with --wait, abort if pull exceeds timeout (in seconds)
+ --async with --wait, don't wait for cancel to finish
+ --keep-relative keep the backing chain relatively referenced
+ --bytes the bandwidth limit is in bytes/s rather than MiB/s
+
+
+ */
+ $snapslist= getvmsnapshots($vm) ;
+ $disks =$lv->get_disk_stats($vm) ;
+ foreach($disks as $disk) {
+ $file = $disk["file"] ;
+ $output = "" ;
+ exec("qemu-img info --backing-chain -U '$file' | grep image:",$output) ;
+ foreach($output as $key => $line) {
+ $line=str_replace("image: ","",$line) ;
+ $output[$key] = $line ;
+ }
+ $snaps[$vm][$disk["device"]] = $output ;
+ $rev = "r".$disk["device"] ;
+ $reversed = array_reverse($output) ;
+ $snaps[$vm][$rev] = $reversed ;
+ }
+ $snaps_json=json_encode($snaps,JSON_PRETTY_PRINT) ;
+ $pathinfo = pathinfo($file) ;
+ $dirpath = $pathinfo["dirname"] ;
+ file_put_contents("$dirpath/image.tracker",$snaps_json) ;
+
+ foreach($disks as $disk) {
+ $path = $disk['file'] ;
+ $cmdstr = "virsh blockpull '$vm' --path '$path' --verbose --pivot --delete" ;
+ $cmdstr = "virsh blockpull '$vm' --path '$path' --verbose --wait " ;
+ # Process disks and update path.
+ $snapdisks=($snapslist[$snap]['disks']) ;
+ if ($base != "--base" && $base != "") {
+ #get file name from snapshot.
+ $snapdisks=($snapslist[$base]['disks']) ;
+ $basepath = "" ;
+ foreach ($snapdisks as $snapdisk) {
+ $diskname = $snapdisk["@attributes"]["name"] ;
+ if ($diskname != $disk['device']) continue ;
+ $basepath = $snapdisk["source"]["@attributes"]["file"] ;
+ }
+ if ($basepath != "") $cmdstr .= " --base '$basepath' ";
+ }
+
+ if ($action) $cmdstr .= " $action ";
+
+
+ $error = execCommand_nchan($cmdstr,$path) ;
+
+ if (!$error) {
+ $arrResponse = ['error' => substr($output[0],6) ] ;
+ return($arrResponse) ;
+ } else {
+ # Remove nvram snapshot
+ $arrResponse = ['success' => true] ;
+ }
+
+ }
+
+ refresh_snapshots_database($vm) ;
+ if($ret)
+ $data = ["error" => "Unable to remove snap metadata $snap"] ;
+ else
+ $data = ["success => 'true"] ;
+
+ return $data ;
+
+}
+
+function vm_blockcopy($vm,$path,$base,$top,$pivot,$action) {
+ /*
+ NAME
+ blockcopy - Start a block copy operation.
+
+ SYNOPSIS
+ blockcopy [--dest ] [--bandwidth ] [--shallow] [--reuse-external] [--blockdev] [--wait] [--verbose] [--timeout ] [--pivot] [--finish] [--async] [--xml ] [--format ] [--granularity ] [--buf-size ] [--bytes] [--transient-job] [--synchronous-writes] [--print-xml]
+
+ DESCRIPTION
+ Copy a disk backing image chain to dest.
+
+ OPTIONS
+ [--domain] domain name, id or uuid
+ [--path] fully-qualified path of source disk
+ --dest path of the copy to create
+ --bandwidth bandwidth limit in MiB/s
+ --shallow make the copy share a backing chain
+ --reuse-external reuse existing destination
+ --blockdev copy destination is block device instead of regular file
+ --wait wait for job to reach mirroring phase
+ --verbose with --wait, display the progress
+ --timeout implies --wait, abort if copy exceeds timeout (in seconds)
+ --pivot implies --wait, pivot when mirroring starts
+ --finish implies --wait, quit when mirroring starts
+ --async with --wait, don't wait for cancel to finish
+ --xml filename containing XML description of the copy destination
+ --format format of the destination file
+ --granularity power-of-two granularity to use during the copy
+ --buf-size maximum amount of in-flight data during the copy
+ --bytes the bandwidth limit is in bytes/s rather than MiB/s
+ --transient-job the copy job is not persisted if VM is turned off
+ --synchronous-writes the copy job forces guest writes to be synchronously written to the destination
+ --print-xml print the XML used to start the copy job instead of starting the job
+ */
+}
+
+
?>
diff --git a/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js b/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js
index 5cea015df..a3907ffe0 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"){
+function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, console="web", preview=false){
var opts = [];
var path = location.pathname;
var x = path.indexOf("?");
@@ -106,6 +106,12 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
e.preventDefault();
ajaxVMDispatch( {action:"domain-destroy", uuid:uuid}, "loadlist");
}});
+ opts.push({divider:true});
+
+ opts.push({text:_("Create Snapshot"), icon:"fa-clone", action:function(e) {
+ e.preventDefault();
+ selectsnapshot(uuid , name, "--generate" , "create",false,state) ;
+ }});
} else if (state == "pmsuspended") {
opts.push({text:_("Resume"), icon:"fa-play", action:function(e) {
e.preventDefault();
@@ -141,7 +147,8 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
ajaxVMDispatchconsoleRV({action:"domain-start-consoleRV", uuid:uuid, vmrcurl:vmrcurl}, "loadlist") ;
}});
}
- }}
+ }
+ }
opts.push({divider:true});
if (log !== "") {
opts.push({text:_("Logs"), icon:"fa-navicon", action:function(e){e.preventDefault(); openTerminal('log',name,log);}});
@@ -149,6 +156,10 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
opts.push({text:_("Edit"), icon:"fa-pencil", href:path+'/UpdateVM?uuid='+uuid});
if (state == "shutoff") {
opts.push({divider:true});
+ opts.push({text:_("Create Snapshot"), icon:"fa-clone", action:function(e) {
+ e.preventDefault();
+ selectsnapshot(uuid , name, "--generate" , "create",false,state) ;
+ }});
opts.push({text:_("Remove VM"), icon:"fa-minus", action:function(e) {
e.preventDefault();
swal({
@@ -182,6 +193,50 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c
}
context.attach('#vm-'+uuid, opts);
}
+function addVMSnapContext(name, uuid, template, state, snapshotname, preview=false){
+ var opts = [];
+ var path = location.pathname;
+ var x = path.indexOf("?");
+ if (x!=-1) path = path.substring(0,x);
+
+ context.settings({right:false,above:false});
+ if (state == "running") {
+
+ opts.push({text:_("Revert snapshot"), icon:"fa-fast-backward", action:function(e) {
+ e.preventDefault();
+ selectsnapshot(uuid, name, snapshotname, "revert",true) ;
+ }});
+
+ 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();
+ selectblock(uuid, name, snapshotname, "commit",true) ;
+ }});
+
+ opts.push({text:_("Block Pull"), 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();
+ selectblock(uuid, name, snapshotname, "pull",true) ;
+ }});
+ if (preview) {
+ opts.push({text:_("Block Copy"), icon:"fa-stop", action:function(e) {
+ e.preventDefault();
+ ajaxVMDispatch({action:"domain-stop", uuid:uuid}, "loadlist");
+ }}); }
+ } else {
+ opts.push({text:_("Revert snapshot"), icon:"fa-fast-backward", action:function(e) {
+ e.preventDefault();
+ $('#vm-'+uuid).find('i').removeClass('fa-play fa-square fa-pause').addClass('fa-refresh fa-spin');
+ selectsnapshot(uuid, name, snapshotname, "revert",true) ;
+ }});
+ }
+ opts.push({text:_("Remove snapshot"), icon:"fa-trash", action:function(e) {
+ e.preventDefault();
+ $('#vm-'+uuid).find('i').removeClass('fa-play fa-square fa-pause').addClass('fa-refresh fa-spin');
+ selectsnapshot(uuid, name, snapshotname, "remove",true) ;
+ }});
+ context.attach('#vmsnap-'+uuid, opts);
+}
function startAll() {
$('input[type=button]').prop('disabled',true);
for (var i=0,vm; vm=kvm[i]; i++) if (vm.state!='running') $('#vm-'+vm.id).parent().find('i').removeClass('fa-square').addClass('fa-refresh fa-spin');
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/VMAjaxCall.php b/emhttp/plugins/dynamix.vm.manager/scripts/VMAjaxCall.php
new file mode 100755
index 000000000..59b846f4d
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/VMAjaxCall.php
@@ -0,0 +1,102 @@
+#!/usr/bin/php -q
+
+
+$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
+
+require_once "$docroot/webGui/include/Wrappers.php";
+
+// add translations
+$_SERVER['REQUEST_URI'] = '';
+$login_locale = _var($display,'locale');
+require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
+require_once "$docroot/webGui/include/Translations.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
+function write(...$messages){
+ $com = curl_init();
+ curl_setopt_array($com,[
+ CURLOPT_URL => 'http://localhost/pub/vmaction?buffer_length=1',
+ CURLOPT_UNIX_SOCKET_PATH => '/var/run/nginx.socket',
+ CURLOPT_POST => 1,
+ CURLOPT_RETURNTRANSFER => true
+ ]);
+ foreach ($messages as $message) {
+ curl_setopt($com, CURLOPT_POSTFIELDS, $message);
+ curl_exec($com);
+ }
+ curl_close($com);
+ }
+function execCommand_nchan($command,$idx) {
+ $waitID = mt_rand();
+ [$cmd,$args] = explode(' ',$command,2);
+ write("","addLog\0","show_Wait\0$waitID");
+ write("addLog\0
") ;
+ write("addToID\0$idx\0 $action") ;
+ $proc = popen("$command 2>&1",'r');
+ while ($out = fgets($proc)) {
+ $out = preg_replace("%[\t\n\x0B\f\r]+%", '',$out);
+ if (substr($out,0,1) == "B") { ;
+ write("progress\0$idx\0".htmlspecialchars(substr($out,strrpos($out,"Block Pull")))) ;
+ } else echo write("addToID\0$idx\0 ".htmlspecialchars($out));
+ }
+ $retval = pclose($proc);
+ $out = $retval ? _('The command failed').'.' : _('The command finished successfully').'!';
+ write("stop_Wait\0$waitID","addLog\0
$out");
+ return $retval===0;
+ }
+
+#{action:"snap-", uuid:uuid , snapshotname:target , remove:remove, free:free ,removemeta:removemeta ,keep:keep, desc:desc}
+#VM ID [ 99]: pull. .Block Pull: [ 0 %]Block Pull: [100 %].Pull complete.
+$url = rawurldecode($argv[1]??'');
+$waitID = mt_rand();
+$style = ["";
+
+foreach (explode('&', $url) as $chunk) {
+ $param = explode("=", $chunk);
+ if ($param) {
+ ${urldecode($param[0])} = urldecode($param[1]) ;
+ }
+}
+$id = 1 ;
+write(implode($style)."");
+$process = " " ;
+write("","addLog\0");
+write("addLog\0".htmlspecialchars("VMName $name "));
+write("addLog\0".htmlspecialchars("SNAP $snapshotname "));
+write("addLog\0".htmlspecialchars("Base $targetbase "));
+if ($action == "commit") {
+ write("addLog\0".htmlspecialchars("Top $targettop "));
+ write("addLog\0".htmlspecialchars("Pivot $targetpivot "));
+ write("addLog\0".htmlspecialchars("Delete $targetdelete "));
+}
+
+switch ($action) {
+ case "commit":
+ vm_blockcommit($name,$snapshotname,$path,$targetbase,$targettop,$targetpivot,$targetdelete) ;
+ break ;
+ case "copy":
+ vm_blockcopy($name,$snapshotname,$path,$targetbase,$targettop,$pivot,' ') ;
+ break;
+ case "pull":
+ vm_blockpull($name,$snapshotname,$path,$targetbase,$targettop,$pivot,' ') ;
+ break ;
+
+ }
+#execCommand_nchan("ls /") ;
+write("stop_Wait\0$waitID") ;
+write('_DONE_','');
+
+?>
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page
index 19af54ec3..e661e5d0f 100644
--- a/emhttp/plugins/dynamix/DashStats.page
+++ b/emhttp/plugins/dynamix/DashStats.page
@@ -181,6 +181,7 @@ div.last{padding-bottom:12px}
div.leftside{float:left;width:66%}
div.rightside{float:right;margin:0;text-align:center}
div[id$=chart]{margin:-12px 8px -24px -18px}
+div.template,div#dialogWindow,input#upload{display:none}
span.green,span.red,span.orange{padding-left:0}
span.ctrl{float:right;margin-top:0;margin-right:10px}
span.ctrl span{font-size:2rem!important}
@@ -814,6 +815,77 @@ var netchart = new ApexCharts(document.querySelector('#netchart'), options_net);
if ($.cookie('port_select')!=null && !ports.includes($.cookie('port_select'))) $.removeCookie('port_select');
var port_select = $.cookie('port_select')||ports[0];
+function selectsnapshot(uuid, name ,snaps, opt, getlist){
+
+ var root = = '"'.$domain_cfg["MEDIADIR"].'"';?>;
+ var match= ".iso";
+ var box = $("#dialogWindow2");
+ box.html($("#templatesnapshot"+opt).html());
+ var height = 200;
+ const Capopt = opt.charAt(0).toUpperCase() + opt.slice(1) ;
+ var optiontext = Capopt + " Snapshot" ;
+ box.find('#VMName').html(name) ;
+ box.find('#targetsnap').val(snaps) ;
+ box.find('#targetsnapl').html(snaps) ;
+ if (getlist) {
+ var only = 1 ;
+ if (opt == "remove") only = 0;
+ $.post("/plugins/dynamix.vm.manager/include/VMajax.php", {action:"snap-images", uuid:uuid , snapshotname:snaps, only:only}, function(data) {
+ if (data.html) {
+ box.find('#targetsnapimages').html(data.html) ;
+ }
+ },'json');
+ }
+
+
+ document.getElementById("targetsnapfspc").checked = true ;
+
+ box.dialog({
+ title: "_("+optiontext+ ")_",
+ resizable: false,
+ width: 600,
+ height: 500,
+ modal: true,
+ show: {effect:'fade', duration:250},
+ hide: {effect:'fade', duration:250},
+
+ buttons: {
+ "_(Proceed)_": function(){
+ var target = box.find('#targetsnap');
+ if (target.length) {
+ target = target.val();
+ if (!target ) {errorTarget(); return;}
+ } else target = '';
+ var remove = 'yes'
+ var keep = 'yes'
+ var removemeta = 'yes'
+ var free = 'yes'
+ var desc = ''
+ box.find('#targetsnap').prop('disabled',true);
+ if (opt == "revert") {
+ const x = box.find('#targetsnaprmv').prop('checked') ;
+ if (x) remove = 'yes' ; else remove = 'no' ;
+ x = box.find('#targetsnaprmvmeta').prop('checked') ;
+ if (x) removemeta = 'yes' ; else removemeta = 'no' ;
+ x = box.find('#targetsnapkeep').prop('checked') ;
+ if (x) keep = 'yes' ; else keep = 'no' ;
+ }
+ if (opt == "create") {
+ const x = box.find('#targetsnapfspc').prop('checked') ;
+ if (x) free = 'yes' ; else free = 'no' ;
+ var desc = box.find("#targetsnapdesc").prop('value') ;
+ }
+ ajaxVMDispatch({action:"snap-" + opt +'-external', uuid:uuid , snapshotname:target , remove:remove, free:free, desc:desc } , "loadlist");
+ box.dialog('close');
+ },
+ "_(Cancel)_": function(){
+ box.dialog('close');
+ }
+ }
+ });
+ dialogStyle();
+}
+
function initCharts(clear) {
$.post('/webGui/include/InitCharts.php',{cmd:'get'},function(data) {
data = JSON.parse(data);
@@ -1504,4 +1576,29 @@ $(function() {
$.post('/webGui/include/InitCharts.php',{cmd:'set',data:JSON.stringify(data)});
});
});
+ function dialogStyle() {
+ $('.ui-dialog-titlebar-close').css({'background':'transparent','border':'none','font-size':'1.8rem','margin-top':'-14px','margin-right':'-18px'}).html('').prop('title');
+ $('.ui-dialog-title').css({'text-align':'center','width':'100%','font-size':'1.8rem'});
+ $('.ui-dialog-content').css({'padding-top':'15px','vertical-align':'bottom'});
+ $('.ui-button-text').css({'padding':'0px 5px'});
+}
+
+
+
+
+
+
+
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout.php b/emhttp/plugins/dynamix/include/DefaultPageLayout.php
index 1bf2f94dc..2bbe7a552 100644
--- a/emhttp/plugins/dynamix/include/DefaultPageLayout.php
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout.php
@@ -342,6 +342,29 @@ function openDocker(cmd,title,plg,func,start=0,button=0) {
$('button.confirm').prop('disabled',button==0);
});
}
+function openVMAction(cmd,title,plg,func,start=0,button=0) {
+ // start = 0 : run command only when not already running (default)
+ // start = 1 : run command unconditionally
+ // button = 0 : hide CLOSE button (default)
+ // button = 1 : show CLOSE button
+ nchan_vmaction.start();
+ $.post('/webGui/include/StartCommand.php',{cmd:cmd,start:start},function(pid) {
+ if (pid==0) {
+ nchan_vmaction.stop();
+ $('div.spinner.fixed').hide();
+ $(".upgrade_notice").addClass('alert');
+ return;
+ }
+ swal({title:title,text:"
",html:true,animation:'none',showConfirmButton:button!=0,confirmButtonText:"=_('Close')?>"},function(close){
+ nchan_vmaction.stop();
+ $('div.spinner.fixed').hide();
+ $('.sweet-alert').hide('fast').removeClass('nchan');
+ setTimeout(function(){bannerAlert("=_('Attention - operation continues in background')?> ["+pid.toString().padStart(8,'0')+"]\" onclick='abortOperation("+pid+")'>",cmd,plg,func,start);});
+ });
+ $('.sweet-alert').addClass('nchan');
+ $('button.confirm').prop('disabled',button==0);
+ });
+}
function abortOperation(pid) {
swal({title:"=_('Abort background operation')?>",text:"=_('This may leave an unknown state')?>",html:true,animation:'none',type:'warning',showCancelButton:true,confirmButtonText:"=_('Proceed')?>",cancelButtonText:"=_('Cancel')?>"},function(){
$.post('/webGui/include/StartCommand.php',{kill:pid},function() {
@@ -915,7 +938,54 @@ function parseINI(data){
}
box.scrollTop(box[0].scrollHeight);
});
-
+var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket'});
+nchan_vmaction.on('message', function(data) {
+ if (!data || openDone(data)) return;
+ var box = $('pre#swaltext');
+ data = data.split('\0');
+ switch (data[0]) {
+ case 'addLog':
+ var rows = document.getElementsByClassName('logLine');
+ if (rows.length) {
+ var row = rows[rows.length-1];
+ row.innerHTML += data[1]+'
';
+ }
+ break;
+ case 'progress':
+ var rows = document.getElementsByClassName('progress-'+data[1]);
+ if (rows.length) {
+ rows[rows.length-1].textContent = data[2];
+ }
+ break;
+ case 'addToID':
+ var rows = document.getElementById(data[1]);
+ if (rows === null) {
+ rows = document.getElementsByClassName('logLine');
+ if (rows.length) {
+ var row = rows[rows.length-1];
+ row.innerHTML += ''+data[1]+': '+data[2]+'.
';
+ }
+ } else {
+ var rows_content = rows.getElementsByClassName('content');
+ if (!rows_content.length || rows_content[rows_content.length-1].textContent != data[2]) {
+ rows.innerHTML += ''+data[2]+'.';
+ }
+ }
+ break;
+ case 'show_Wait':
+ progress_span[data[1]] = document.getElementById('wait-'+data[1]);
+ progress_dots[data[1]] = setInterval(function(){if (((progress_span[data[1]].innerHTML += '.').match(/\./g)||[]).length > 9) progress_span[data[1]].innerHTML = progress_span[data[1]].innerHTML.replace(/\.+$/,'');},500);
+ break;
+ case 'stop_Wait':
+ clearInterval(progress_dots[data[1]]);
+ progress_span[data[1]].innerHTML = '';
+ break;
+ default:
+ box.html(box.html()+data[0]);
+ break;
+ }
+ box.scrollTop(box[0].scrollHeight);
+});
var backtotopoffset = 250;
var backtotopduration = 500;
$(window).scroll(function() {
diff --git a/etc/rc.d/rc.libvirt b/etc/rc.d/rc.libvirt
index 17f0900a4..7b45c4c85 100755
--- a/etc/rc.d/rc.libvirt
+++ b/etc/rc.d/rc.libvirt
@@ -175,6 +175,10 @@ start_libvirtd() {
cp -n /etc/libvirt-/*.conf /etc/libvirt &> /dev/null
# ensure tpm-states path exists
mkdir -p /etc/libvirt/qemu/swtpm/tpm-states
+ # Setup Snapshot persistance.
+ mkdir -p /etc/libvirt/qemu/snapshot/
+ rm -r /var/lib/libvirt/qemu/snapshot/
+ ln -fs /etc/libvirt/qemu/snapshot/ /var/lib/libvirt/qemu/
echo "Starting libvirtd..."
mkdir -p $(dirname $LIBVIRTD_PIDFILE)
check_processor