diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..b3d3e58c9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "natizyskunk.sftp", + "davidanson.vscode-markdownlint", + "bmewburn.vscode-intelephense-client", + "foxundermoon.shell-format", + "timonwong.shellcheck", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..efd806383 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "*.page": "php" + }, + "prettier.tabWidth": 4, + "editor.tabSize": 4, + "editor.indentSize": "tabSize" +} diff --git a/.vscode/sftp-template.json b/.vscode/sftp-template.json new file mode 100644 index 000000000..2e103fd3c --- /dev/null +++ b/.vscode/sftp-template.json @@ -0,0 +1,36 @@ +{ + "_comment": "rename this file to .vscode/sftp.json and replace name/host/privateKeyPath for your system", + "name": "Tower", + "host": "Tower.local", + "protocol": "sftp", + "port": 22, + "username": "root", + "privateKeyPath": "C:/Users/username/.ssh/tower", + "remotePath": "/usr/local", + "uploadOnSave": true, + "useTempFile": false, + "openSsh": false, + "ignore": [ + "// comment: ignore dot files/dirs in root of repo", + ".vscode", + ".git", + ".DS_Store", + "// comment: ignore symlinks in repo", + "sbin/emcmd", + "sbin/plugin", + "sbin/language", + "sbin/newperms", + "sbin/inet", + "sbin/samba", + "sbin/diagnostics", + "emhttp/boot", + "emhttp/state", + "emhttp/mnt", + "emhttp/log", + "emhttp/plugins/dynamix/images/case-model.png", + "emhttp/webGui", + "// comment: ignore files distributed by Unraid Connect", + "emhttp/plugins/dynamix.my.servers", + "emhttp/plugins/dynamix/include/UpdateDNS.php" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..17a862b7f --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Unraid webgui repo + +## Structure + +The emhttp, etc, sbin, and src dirs in this repo are extracted to the /usr/local/ directory when an Unraid release is built. + +## Contributions + +Please be aware that there is a fairly high barrier to entry when working on the Unraid webgui. It has grown organically over the years and you will need to do quite a bit of reverse engineering to understand it. + +If you choose to proceed, clone this repo locally, test edits by copying the changes to your server, then submit a PR to when fully tested. + +Be sure to describe (in the Unraid forum, a GitHub issue, or the GitHub PR) the problem/feature you are working on and explain how the proposed change solves it. + +We recommend using VS Code with the following plugins: + +* Install [natizyskunk.sftp](https://marketplace.visualstudio.com/items?itemName=Natizyskunk.sftp) and use .vscode/sftp-template.json as a template for creating your personalized sftp.json file to have VS Code upload files to your server when they are saved. Your personalized sftp.json file is blocked by .gitignore so it should never be included in your PR. +* Install [bmewburn.vscode-intelephense-client](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client) and when reasonable, implement its recommendations for PHP code. +* Install [DavidAnson.vscode-markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) and when reasonable, implement its recommendations for markdown code. +* Install [foxundermoon.shell-format](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format) and [timonwong.shellcheck](https://marketplace.visualstudio.com/items?itemName=timonwong.shellcheck) and when reasonable, implement their recommendations when making changes to bash scripts. +* Install [esbenp.prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and when reasonable, use it for formatting files. diff --git a/emhttp/plugins/dynamix.apcupsd/event/driver_loaded b/emhttp/plugins/dynamix.apcupsd/event/driver_loaded index ab6c7ba10..622033049 100755 --- a/emhttp/plugins/dynamix.apcupsd/event/driver_loaded +++ b/emhttp/plugins/dynamix.apcupsd/event/driver_loaded @@ -6,7 +6,10 @@ cfg=/boot/config/plugins/dynamix.apcupsd/dynamix.apcupsd.cfg [[ -f /var/run/apcupsd.pid || ! -f $cfg ]] && exit # Read settings -source $cfg +. $cfg + +# run & log functions +. /etc/rc.d/rc.runlog # Apply settings sed -i -e '/^NISIP/c\\NISIP 0.0.0.0' $conf @@ -27,4 +30,4 @@ else fi # Start daemon -[[ $SERVICE == enable ]] && /etc/rc.d/rc.apcupsd start |& logger +[[ $SERVICE == enable ]] && /etc/rc.d/rc.apcupsd start |& log diff --git a/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php b/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php index 72828fe75..8279f31c4 100644 --- a/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php +++ b/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php @@ -368,7 +368,7 @@ class DockerTemplates { @copy($iconRAM,$icon); } if ( !is_file($iconRAM) ) { - exec("/usr/bin/logger ".escapeshellarg("$imageName: Could not download icon $imgUrl")); + exec("logger -t webGUI -- \"$imageName: Could not download icon $imgUrl\""); } return (is_file($iconRAM)) ? str_replace($docroot, '', $iconRAM) : ''; diff --git a/emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php b/emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php index ecf97df96..a3a92ba2a 100644 --- a/emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php +++ b/emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php @@ -20,6 +20,9 @@ require_once "$docroot/webGui/include/Secure.php"; $_SERVER['REQUEST_URI'] = 'plugins'; require_once "$docroot/webGui/include/Translations.php"; +$tmpdir="/boot/deletemedowngrade.".uniqid(); +mkdir($tmpdir); +exec("mv -f /boot/bz* $tmpdir"); exec("mv -f /boot/previous/* /boot"); $version = unscript(_var($_GET,'version')); file_put_contents("$docroot/plugins/unRAIDServer/README.md","**"._('DOWNGRADE TO VERSION')." $version**"); diff --git a/emhttp/plugins/dynamix.plugin.manager/scripts/language b/emhttp/plugins/dynamix.plugin.manager/scripts/language index b613f3c43..59874ad26 100755 --- a/emhttp/plugins/dynamix.plugin.manager/scripts/language +++ b/emhttp/plugins/dynamix.plugin.manager/scripts/language @@ -151,7 +151,7 @@ function download($url, $name, &$error) { // Deal with logging message. // function logger($message) { - shell_exec("logger $message"); + exec("logger -t 'language-manager' -- \"$message\""); } // Interpret a language file @@ -314,7 +314,7 @@ if ($method == 'install') { copy($xml_file, $lang_file); symlink($lang_file, $link_file); write("language: $lang language pack installed\n"); - logger("language: $lang language pack installed"); + logger("$lang language pack installed"); // run hook scripts for post processing post_hooks(); done(0); @@ -396,7 +396,7 @@ if ($method == 'update') { copy($xml_file, $lang_file); symlink($lang_file, $link_file); write("language: $lang language pack updated\n"); - logger("language: $lang language pack updated"); + logger("$lang language pack updated"); // run hook scripts for post processing post_hooks(); done(0); @@ -423,7 +423,7 @@ if ($method == 'remove') { done(1); } write("language: $lang language pack removed\n"); - logger("language: $lang language pack removed"); + logger("$lang language pack removed"); // run hook scripts for post processing post_hooks(); done(0); diff --git a/emhttp/plugins/dynamix.plugin.manager/scripts/multiplugin b/emhttp/plugins/dynamix.plugin.manager/scripts/multiplugin index 1f750aaae..0cab2e0c3 100755 --- a/emhttp/plugins/dynamix.plugin.manager/scripts/multiplugin +++ b/emhttp/plugins/dynamix.plugin.manager/scripts/multiplugin @@ -44,7 +44,7 @@ foreach ($plugins as $plugin) { $run = popen("$cmd $method $pluginArg",'r'); while (!feof($run)) { $line .= fgetc($run); - if (in_array($line[-1],["\r","\n"])) {write($line); $line = '';} + if (!empty($line) && in_array($line[-1],["\r","\n"])) {write($line); $line = '';} } pclose($run); write("\n"); diff --git a/emhttp/plugins/dynamix.plugin.manager/scripts/plugin b/emhttp/plugins/dynamix.plugin.manager/scripts/plugin index e2a60ccc5..9de7c7db3 100755 --- a/emhttp/plugins/dynamix.plugin.manager/scripts/plugin +++ b/emhttp/plugins/dynamix.plugin.manager/scripts/plugin @@ -154,7 +154,7 @@ function error_desc($code) { case 2: return 'Parse error'; case 3: return 'File I/O error'; case 4: return 'Network failure'; - case 5: return 'SSL verification failure'; + case 5: return 'SSL verification failure - Check the date and time of your server in Settings - Date And Time'; case 6: return 'Username/password authentication failure'; case 7: return 'Protocol errors'; case 8: return 'Invalid URL / Server error response'; @@ -284,7 +284,7 @@ function filter_url($url) { // Deal with logging message. // function logger($message) { - shell_exec("logger $message"); + exec("logger -t 'plugin-manager' -- \"$message\""); } // Interpret a plugin file @@ -382,12 +382,12 @@ function plugin($method, $plugin_file, &$error) { // If file already exists, check the SHA256/MD5 (if supplied) if (file_exists($name)) { if ($file->SHA256) { - logger("plugin: checking: $name - SHA256"); + logger("checking: $name - SHA256"); if (hash_file('sha256', $name) != $file->SHA256) { unlink($name); } } elseif ($file->MD5) { - logger("plugin: checking: $name - MD5"); + logger("checking: $name - MD5"); if (md5_file($name) != $file->MD5) { unlink($name); } @@ -396,12 +396,12 @@ function plugin($method, $plugin_file, &$error) { // If file already exists, do not overwrite // if (file_exists($name)) { - logger("plugin: skipping: $name already exists"); + logger("skipping: $name already exists"); } elseif ($file->LOCAL) { // Create the file // // for local file, just copy it - logger("plugin: creating: $name - copying LOCAL file $file->LOCAL"); + logger("creating: $name - copying LOCAL file $file->LOCAL"); if (!copy($file->LOCAL, $name)) { $error = "unable to copy LOCAL file: $name"; @unlink($name); @@ -409,10 +409,10 @@ function plugin($method, $plugin_file, &$error) { } } elseif ($file->INLINE) { // for inline file, create with inline contents - logger("plugin: creating: $name - from INLINE content"); + logger("creating: $name - from INLINE content"); $contents = trim($file->INLINE).PHP_EOL; if ($file->attributes()->Type == 'base64') { - logger("plugin: decoding: $name as base64"); + logger("decoding: $name as base64"); $contents = base64_decode($contents); if ($contents === false) { $error = "unable to decode inline base64: $name"; @@ -426,20 +426,20 @@ function plugin($method, $plugin_file, &$error) { } } elseif ($file->URL) { // for download file, download and maybe verify the file MD5 - logger("plugin: creating: $name - downloading from URL $file->URL"); + logger("creating: $name - downloading from URL $file->URL"); if ( (download($file->URL, $name, $error) === false) && (download(filter_url($file->URL), $name, $error) === false) ) { @unlink($name); return false; } if ($file->SHA256) { - logger("plugin: checking: $name - SHA256"); + logger("checking: $name - SHA256"); if (hash_file('sha256', $name) != $file->SHA256) { $error = "bad file SHA256: $name"; unlink($name); return false; } } elseif ($file->MD5) { - logger("plugin: checking: $name - MD5"); + logger("checking: $name - MD5"); if (md5_file($name) != $file->MD5) { $error = "bad file MD5: $name"; unlink($name); @@ -452,7 +452,7 @@ function plugin($method, $plugin_file, &$error) { if ($file->attributes()->Mode) { // if file has 'Mode' attribute, apply it $mode = $file->attributes()->Mode; - logger("plugin: setting: $name - mode to $mode"); + logger("setting: $name - mode to $mode"); if (!chmod($name, octdec($mode))) { $error = "chmod failure: $name"; return false; @@ -464,13 +464,13 @@ function plugin($method, $plugin_file, &$error) { if ($file->attributes()->Run) { $command = $file->attributes()->Run; if ($name) { - logger(escapeshellarg("plugin: running: $command $name")); + logger("running: $command $name"); $retval = run("$command $name"); } elseif ($file->LOCAL) { - logger(escapeshellarg("plugin: running: $command $file->LOCAL")); + logger("running: $command $file->LOCAL"); $retval = run("$command $file->LOCAL"); } elseif ($file->INLINE) { - logger("plugin: running: 'anonymous'"); + logger("running: 'anonymous'"); $name = '/tmp/inline.sh'; file_put_contents($name, $file->INLINE); $retval = run("$command $name"); @@ -718,7 +718,7 @@ if ($method == 'install') { if ($target != $plugin_file) copy($plugin_file, $target); symlink($target, $symlink); write("plugin: $plugin installed\n"); - logger("plugin: $plugin installed"); + logger("$plugin installed"); } else { write("script: $plugin executed\n"); logger("script: $plugin executed"); @@ -835,7 +835,7 @@ if ($method == 'update') { copy($plugin_file, $target); symlink($target, $symlink); write("plugin: $plugin updated\n"); - logger("plugin: $plugin updated"); + logger("$plugin updated"); // run hook scripts for post processing post_hooks(); done(0); @@ -867,7 +867,7 @@ if ($method == 'remove') { // remove the plugin file move($installed_plugin_file, "$boot-removed"); write("plugin: $plugin removed\n"); - logger("plugin: $plugin removed"); + logger("$plugin removed"); exec("/usr/local/sbin/update_cron"); // run hook scripts for post processing post_hooks(); diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page index dceeed474..f34aebf5b 100644 --- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page +++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page @@ -18,6 +18,7 @@ Markdown="false" */ ?> - +
_(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 = ; + 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 = ; @@ -203,8 +252,220 @@ function getisoimage(uuid,dev,bus,file){ }); dialogStyle(); } +function VMClone(uuid, name){ + + //var root = ; + var match= ".iso"; + var box = $("#dialogWindow"); + var height = 200; + box.html($("#templateClone").html()); + box.find('#VMBeingCloned').html(name).change() ; + box.find('#target').val(name + "_clone") ; + document.getElementById("Free").checked = true ; + document.getElementById("Overwrite").checked = true ; + var Cancel = _("Cancel") ; + 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(); +} + + +function selectsnapshot(uuid, name ,snaps, opt, getlist,state){ + + var root = ; + 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 = ; + 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 +565,119 @@ $(function() {
_(ISO Image)_: : +
+ +
+_(CD1 ISO Image)_: +:
+_(CD2 ISO Image)_: +: +
+ +
+
+ +

+ + + +
_(VM Name)_: +
_(Snapshot Name)_: + +_(Check free space)_: +
_(Description )_: +
+
+ +
+_(VM Name)_: + +
+_(Snapshot Name)_: + +
+_(Remove Images)_: + +_(Remove Meta)_: + + +
+ +
+ +
+_(!! Warning removing Snapshots can break the chain !!)_

+ +_(VM Name)_: + +
+_(Snapshot Name)_: + +
+ +
+ +
+_(VM Name)_: + +
+_(Snapshot Name)_: + +
+



+ + + + +
_(Base Image)_: +
_(Top Image )_: + +
_(Pivot)_: +
_(Delete)_: +
+
+
+
+ +
+
+lang=""> + + + + + + + +"> + + +
+ + + + + + + + + + + + + + + + + + + +
_(VM Being Cloned)_:
_(New VM)_:
_(Overwrite)_:
_(Check Free Space)_:
+ + +
+
\ 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..27e19341c 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/webGui/include/Helpers.php"; 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 @@ foreach ($vms as $vm) { $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 @@ 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')\"", 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 @@ foreach ($vms as $vm) { } /* 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,13 +192,13 @@ foreach ($vms as $vm) { } /* 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']; + $disk = $arrCD['file'] ?? $arrCD['partition'] ?? "" ; $dev = $arrCD['device']; $bus = $arrValidDiskBuses[$arrCD['bus']] ?? 'VirtIO'; - $boot= $arrCD["boot order"]; + $boot= $arrCD["boot order"] ?? "" ; if ($boot < 1) $boot="Not set"; if ($disk != "" ) { $title = _("Eject CD Drive")."."; @@ -214,12 +238,43 @@ foreach ($vms as $vm) { } } } - } else { + } + else { 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 b1522da9e..ed28dffe0 100644 --- a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php +++ b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php @@ -265,6 +265,40 @@ case 'change-media': : ['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) @@ -302,6 +336,40 @@ case 'snap-create': : ['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 3455c1e52..484f5e011 100644 --- a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php +++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php @@ -267,7 +267,7 @@ } - function config_to_xml($config) { + function config_to_xml($config,$vmclone = false) { $domain = $config['domain']; $media = $config['media']; $nics = $config['nic']; @@ -463,19 +463,26 @@ $usbstr = ''; if (!empty($usb)) { foreach($usb as $i => $v){ - $usbx = explode(':', $v); + if ($vmclone) $usbx = explode(':', $v['id']); else $usbx = explode(':', $v); $startupPolicy = '' ; - if (isset($usbopt[$v])) { + if (isset($usbopt[$v]) && !$vmclone ) { if (strpos($usbopt[$v], "#remove") == false) $startupPolicy = 'startupPolicy="optional"' ; else $startupPolicy = '' ; - } + } + if (isset($v["startupPolicy"]) && $vmclone ) { + if ($v["startupPolicy"] == "optional" ) $startupPolicy = 'startupPolicy="optional"' ; else $startupPolicy = '' ; + } + $usbstr .= " " ; - if (!empty($usbboot[$v])) { + if (!empty($usbboot[$v]) && !$vmclone ) { $usbstr .= "" ; - } + } + if (isset($v["usbboot"]) && $vmclone ) { + if ($v["usbboot"] != NULL) $usbstr .= "" ; + } $usbstr .= ""; } } @@ -774,17 +781,20 @@ if ($strAutoport == "yes") $strPort = $strWSport = "-1" ; if (($gpu['copypaste'] == "yes") && ($strProtocol == "spice")) $vmrcmousemode = "" ; else $vmrcmousemode = "" ; + if ($strProtocol == "spice") $virtualaudio = "spice" ; else $virtualaudio = "none" ; $vmrc = " - + $vmrcmousemode "; + +