diff --git a/emhttp/plugins/dynamix.vm.manager/CloneVM.page b/emhttp/plugins/dynamix.vm.manager/CloneVM.page deleted file mode 100644 index b74cbccbc..000000000 --- a/emhttp/plugins/dynamix.vm.manager/CloneVM.page +++ /dev/null @@ -1,111 +0,0 @@ -Title="Clone VM" -Tag="clipboard" -Cond="(pgrep('libvirtd')!==false)" -Markdown="false" ---- - - - - - - -
-
-lang=""> - - - - - - - -"> - - -_(VM Being Cloned)_: -
- -_(New VM)_: -
- -_(Overwrite)_: -
- -_(Start Cloned VM)_: -
- -_(Edit VM after clone)_: -
- - - -
\ No newline at end of file diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page index e53dbacea..06940322d 100644 --- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page +++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page @@ -305,4 +305,42 @@ $(function() { _(ISO Image)_: : + + +
+
+lang=""> + + + + + + + +"> + + +
+ + + + + + + + + + + + + + + + + + + +
_(VM Being Cloned)_:
_(New VM)_:
_(Overwrite)_:
_(Start Cloned VM)_:
_(Edit VM after clone)_:
_(Check Free Space)_:
+ +
\ No newline at end of file diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMclone.php b/emhttp/plugins/dynamix.vm.manager/include/VMclone.php deleted file mode 100644 index 233b74256..000000000 --- a/emhttp/plugins/dynamix.vm.manager/include/VMclone.php +++ /dev/null @@ -1,272 +0,0 @@ - - '', - 'icon' => $arrAllTemplates[$strSelectedTemplate]['icon'], - 'autostart' => false, - 'form' => $arrAllTemplates[$strSelectedTemplate]['form'], - 'state' => 'shutoff' -]; -$strIconURL = '/plugins/dynamix.vm.manager/templates/images/'.$arrLoad['icon']; - -if (isset($_GET['uuid'])) { - // Edit VM mode - $res = $lv->domain_get_domain_by_uuid(unscript($_GET['uuid'])); - - if ($res === false) { - echo "

"._('Invalid VM to edit').".

"; - return; - } - - $strIconURL = $lv->domain_get_icon_url($res); - $arrLoad = [ - 'name' => $lv->domain_get_name($res), - 'icon' => basename($strIconURL), - 'autostart' => $lv->domain_get_autostart($res), - 'form' => $arrAllTemplates[$strSelectedTemplate]['form'], - 'state' => $lv->domain_get_state($res) - ]; - - if (empty($_GET['template'])) { - // read vm-template attribute - $strTemplateOS = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@os'); - $strLibreELEC = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@libreelec'); - $strOpenELEC = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@openelec'); - if ($strLibreELEC) $strSelectedTemplate = 'LibreELEC'; - elseif ($strOpenELEC) $strSelectedTemplate = 'OpenELEC'; - elseif ($strTemplateOS) { - $strSelectedTemplate = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@name'); - } else { - // legacy VM support for <6.2 but need it going forward too - foreach ($arrAllTemplates as $strName => $arrTemplate) { - if (!empty($arrTemplate) && !empty($arrTemplate['os']) && $arrTemplate['os'] == $strTemplateOS) { - $strSelectedTemplate = $strName; - break; - } - } - } - if (empty($strSelectedTemplate) || empty($arrAllTemplates[$strSelectedTemplate])) { - $strSelectedTemplate = 'Windows 10'; //default to Windows 10 - } - } - $arrLoad['form'] = $arrAllTemplates[$strSelectedTemplate]['form']; -} -?> - - - - - - - -
-
- - - - - - - - -
_(Icon)_: - - -
-
- '/plugins/dynamix.vm.manager/templates/images/', - "$docroot/boot/config/plugins/dynamix.vm.manager/templates/images/*.png" => '/boot/config/plugins/dynamix.vm.manager/templates/images/' - ]; - foreach ($arrImagePaths as $strGlob => $strIconURLBase) { - foreach (glob($strGlob) as $png_file) { - echo '

'.basename($png_file,'.png').'

'; - } - } - ?> -
-
-
- - - - - - -
_(Autostart)_:
>
-
-

If you want this VM to start with the array, set this to yes.

-
- -
'.parse_file("$docroot/plugins/dynamix.vm.manager/templates/{$arrLoad['form']}",false))?>
- -
-
- - - - - - diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php index 7bf82d5cd..3a2efc8ee 100644 --- a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php +++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php @@ -1454,4 +1454,115 @@ private static $encoding = 'UTF-8'; if ($spicevmc || $qemuvdaagent) $copypaste = true ; else $copypaste = false ; return $copypaste ; } + + function vm_clone($vm, $clone ,$overwrite,$start,$edit, $free, $waitID) { + global $lv,$domain_cfg ; + /* + Clone. + + Stopped only. + + Get new VM Name + Extract XML for VM to be cloned. + Check if directory exists. + Check for disk space + + Stop VM Starting until clone is finished or fails. + + Create new directory for Clone. + Update paths with new directory + + Create new UUID + Create new MAC Address for NICs + + Create VM Disks from source. Options full or Sparce. Method of copy? + + release orginal VM to start. + + If option to edit, show VMUpdate + */ + + #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? + + + + $disks =$lv->get_disk_stats($vm) ; + + $capacity = 0 ; + + 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"] ; + + #Check free space. + write("addLog\0".htmlspecialchars("Checking for free space")); + $dirfree = disk_free_space($pathinfo["dirname"]) ; + + $capacity *= 1 ; + + #if ($free == "yes" && $dirfree < $capacity) { $arrResponse = ['error' => _("Insufficent Storage for Clone")]; return $arrResponse ;} + + #Clone XML + $res = $lv->domain_get_domain_by_uuid($uuid); + + $strXML = $lv->domain_get_xml($res); + $uuid = $lv->domain_get_uuid($vm) ; + var_dump($uuid) ; + $config=domain_to_config($uuid) ; + + + #$config = array_replace_recursive($arrConfigDefaults, domain_to_config($uuid)); + #$config = domain_to_config($uuid); + $config["domain"]["name"] = $clone ; + $config["domain"]["uuid"] = $lv->domain_generate_uuid() ; + $config["nic"]["0"]["mac"] = $lv->generate_random_mac_addr() ; + $config["domain"]["type"] = "kvm"; + + + $files_exist = false ; + foreach ($config["disk"] as $diskid => $disk) { + $config["disk"][$diskid]["new"] = str_replace($name,$clonename,$config["disk"][$diskid]["new"]) ; + #var_dump(pathinfo($config["disk"][$diskid]["new"])) ; + $pi = pathinfo($config["disk"][$diskid]["new"]) ; + $isdir = is_dir($pi['dirname']) ; + if (is_file($config["disk"][$diskid]["new"])) $file_exists = true ; + #var_dump($isdir,$pi['dirname']) ; + } + + $clonedir = $domain_cfg['DOMAINDIR'].$clone ; + #write("addLog\0".htmlspecialchars("Overwrite $overwrite Start $start Edit $edit Check Freespace $free")); + write("addLog\0".htmlspecialchars("Checking for image files")); + #if ($file_exists && $overwrite != "yes") { $arrResponse = ['error' => _("New image file names exist and Overwrite is no")]; return $arrResponse ;} + + write("addLog\0".htmlspecialchars("Creating new XML $clone")); + $xml = $lv->config_to_xml($config) ; + file_put_contents("/tmp/xml" ,$xml) ; + + foreach($disks as $disk) { + $cmdstr = "ls" ; + $error = execCommand_nchan($cmdstr,$path) ; + if (!$error) { + $arrResponse = ['error' => substr($output[0],6) ] ; + return($arrResponse) ; + } else { + $arrResponse = ['success' => true] ; + } + + } + $arrResponse = ['error' => _("Insufficent Storage for Clone")]; + return$arrResponse ; + + } + ?> diff --git a/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js b/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js index 15706b2c7..ab393079c 100644 --- a/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js +++ b/emhttp/plugins/dynamix.vm.manager/javascript/vmmanager.js @@ -2,41 +2,6 @@ function displayconsole(url) { window.open(url, '_blank', 'scrollbars=yes,resizable=yes'); } -function getCloneName(name){ - var root = "" ; - var match= ".iso" ; - var box = $("#dialogWindow"); - box.html($("#templateClone").html()); - - box.find('#VMBeingCloned').html(name).change() ; - - var height = 100; - box.dialog({ - title: "Enter Clone Name", - resizable: false, - width: 600, - height: 300, - modal: true, - show: {effect:'fade', duration:250}, - hide: {effect:'fade', duration:250}, - buttons: { - "_(Insert)_": function(){ - var target = box.find('#target'); - if (target.length) { - target = target.val(); - if (!target ) {errorTarget(); return;} - } else target = ''; - box.find('#target').prop('disabled',true); - ajaxVMDispatch({action:"change-media", uuid:uuid , cdrom:"" , dev:dev , bus:bus , file:target}, "loadlist"); ; - box.dialog('close'); - }, - "_(Cancel)_": function(){ - box.dialog('close'); - } - } - }); - dialogStyle(); -} function downloadFile(source) { var a = document.createElement('a'); a.setAttribute('href',source); @@ -182,25 +147,12 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, c opts.push({text:_("Logs"), icon:"fa-navicon", action:function(e){e.preventDefault(); openTerminal('log',name,log);}}); } opts.push({text:_("Edit"), icon:"fa-pencil", href:path+'/UpdateVM?uuid='+uuid}); - opts.push({text:_("Clone2"), icon:"fa-clone", href:path+'/CloneVM?uuid='+uuid}); if (state == "shutoff") { opts.push({text:_("Clone"), icon:"fa-clone", action:function(e) { e.preventDefault(); - var clonename = getCloneName(name) ; - /*swal({ - title:_("Enter"), - text:_("From VM:")+name, - type:"warning", - showCancelButton:true, - confirmButtonText:_('Proceed'), - cancelButtonText:_('Cancel') - },function(){ - $('#vm-'+uuid).find('i').removeClass('fa-play fa-square fa-pause').addClass('fa-refresh fa-spin'); - ajaxVMDispatch({action:"domain-clone", uuid:uuid }, "loadlist"); - }) ;*/ - + var clonename = VMClone(uuid,name) ; }}); opts.push({divider:true}); @@ -280,3 +232,95 @@ function addVM() { if (x!=-1) path = path.substring(0,x); location = path+"/VMTemplates"; } + + +function getCloneName(uuid,name){ + var root = "" ; + var match= ".iso" ; + var box = $("#dialogWindow"); + box.html($("#templateClone").html()); + + box.find('#VMBeingCloned').html(name).change() ; + + var height = 100; + box.dialog({ + title: "Enter Clone Name", + resizable: false, + width: 600, + height: 300, + modal: true, + show: {effect:'fade', duration:250}, + hide: {effect:'fade', duration:250}, + buttons: { + "Clone": function(){ + var target = box.find('#target'); + if (target.length) { + target = target.val(); + if (!target ) {errorTarget(); return;} + } else target = ''; + box.find('#target').prop('disabled',true); + ajaxVMDispatch({action:"domain-clone", uuid:uuid , clone:target}, "loadlist"); ; + box.dialog('close'); + }, + "Cancel": function(){ + box.dialog('close'); + } + } + }); + dialogStyle(); +} + +function VMClone(uuid, name){ + + //var root = ; + var match= ".iso"; + var box = $("#dialogWindow"); + box.html($("#templateblock").html()); + var height = 200; + box.html($("#templateClone").html()); + + box.find('#VMBeingCloned').html(name).change() ; + + //document.getElementById("targetsnaprmv").checked = true ; + //document.getElementById("targetsnaprmvmeta").checked = true ; + //document.getElementById("targetsnapkeep").checked = true ; + //document.getElementById("targetsnapfspc").checked = true ; + + box.dialog({ + title: "_(VM Clone)_", + resizable: false, + width: 600, + height: 500, + modal: true, + show: {effect:'fade', duration:250}, + hide: {effect:'fade', duration:250}, + buttons: { + "Clone" : function(){ + var target = box.find('#target'); + if (target.length) { + target = target.val(); + if (!target ) {errorTarget(); return;} + } else target = ''; + + var clone = box.find("#target").prop('value') ; + + + x = box.find('#Start').prop('checked') ; + if (x) start = 'yes' ; else start = 'no' ; + x = box.find('#Edit').prop('checked') ; + if (x) edit = 'yes' ; else edit = 'no' ; + x = box.find('#Overwrite').prop('checked') ; + if (x) overwrite = 'yes' ; else overwrite = 'no' ; + x = box.find('#Free').prop('checked') ; + if (x) free = 'yes' ; else free = '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})) ; + openVMAction((scripturl),"VM Clone", "dynamix.vm.manager", "loadlist") ; + box.dialog('close'); + }, + "_(Cancel)_": function(){ + box.dialog('close'); + } + } + }); + dialogStyle(); +} \ No newline at end of file diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php b/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php new file mode 100644 index 000000000..50d32782d --- /dev/null +++ b/emhttp/plugins/dynamix.vm.manager/scripts/VMClone.php @@ -0,0 +1,90 @@ +#!/usr/bin/php -q + + '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
"._('Command execution')."".basename($cmd).' '.str_replace(" -","
  -",htmlspecialchars($args))."
"._('Please wait')."

","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
"._("Options for Block $action").":

"._('Please wait')."
"); +write("addLog\0".htmlspecialchars("VMName $name ")); +write("addLog\0".htmlspecialchars("Clone $clone ")); +write("addLog\0".htmlspecialchars("Overwrite $overwrite Start $start Edit $edit Check Freespace $free")); + +switch ($action) { + case "clone": + vm_clone($name,$clone,$overwrite,$start,$edit,$free,$waitID) ; + break ; + } +#execCommand_nchan("ls /") ; +write("stop_Wait\0$waitID") ; +write('_DONE_',''); + +?> \ No newline at end of file diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout.php b/emhttp/plugins/dynamix/include/DefaultPageLayout.php index 1bf2f94dc..f3e67955f 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:""},function(close){ + nchan_vmaction.stop(); + $('div.spinner.fixed').hide(); + $('.sweet-alert').hide('fast').removeClass('nchan'); + setTimeout(function(){bannerAlert(" ["+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:"",text:"",html:true,animation:'none',type:'warning',showCancelButton:true,confirmButtonText:"",cancelButtonText:""},function(){ $.post('/webGui/include/StartCommand.php',{kill:pid},function() { @@ -915,6 +938,54 @@ nchan_docker.on('message', function(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;