VMs: preserve XML custom settings in VM conifg updates

This commit is contained in:
bergware
2018-08-13 18:28:48 +02:00
parent e3c12d3c6a
commit 0935e480f4
3 changed files with 256 additions and 149 deletions
@@ -39,7 +39,6 @@ if (!empty($_GET['uuid'])) {
}
$strIconURL = $lv->domain_get_icon_url($res);
$arrLoad = [
'name' => $lv->domain_get_name($res),
'icon' => basename($strIconURL),
@@ -49,14 +48,12 @@ if (!empty($_GET['uuid'])) {
];
if (empty($_GET['template'])) {
// read vm-template attribute
$strTemplateOS = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@os');
if (empty($strTemplateOS)) {
$strTemplate = $lv->_get_single_xpath_result($res, '//domain/metadata/*[local-name()=\'vmtemplate\']/@name');
if (!empty($strTemplate)) {
$strSelectedTemplate = $strTemplate;
}
if ($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
// 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;
@@ -65,12 +62,11 @@ if (!empty($_GET['uuid'])) {
}
}
if (empty($strSelectedTemplate) || empty($arrAllTemplates[$strSelectedTemplate])) {
$strSelectedTemplate = 'Custom';
$strSelectedTemplate = 'Windows 10'; //default to Windows 10
}
}
$arrLoad['form'] = $arrAllTemplates[$strSelectedTemplate]['form'];
}
?>
<link type="text/css" rel="stylesheet" href="/plugins/dynamix.vm.manager/styles/dynamix.vm.manager.css">
<link type="text/css" rel="stylesheet" href="/webGui/styles/jquery.filetree.css">
@@ -1,6 +1,7 @@
<?PHP
/* Copyright 2005-2017, Lime Technology
* Copyright 2015-2017, Derek Macias, Eric Schultz, Jon Panozzo.
/* Copyright 2005-2018, Lime Technology
* Copyright 2015-2018, Derek Macias, Eric Schultz, Jon Panozzo.
* Copyright 2012-2018, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -13,6 +14,7 @@
<?
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once "$docroot/webGui/include/Helpers.php";
require_once "$docroot/webGui/include/Custom.php";
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
$arrValidMachineTypes = getValidMachineTypes();
@@ -93,98 +95,54 @@
'target' => ''
]
]
];
];
// Merge in any default values from the VM template
if (!empty($arrAllTemplates[$strSelectedTemplate]) && !empty($arrAllTemplates[$strSelectedTemplate]['overrides'])) {
if ($arrAllTemplates[$strSelectedTemplate] && $arrAllTemplates[$strSelectedTemplate]['overrides']) {
$arrConfigDefaults = array_replace_recursive($arrConfigDefaults, $arrAllTemplates[$strSelectedTemplate]['overrides']);
}
if (array_key_exists('updatevm', $_POST) && !empty($_POST['domain']['uuid'])) {
$_GET['uuid'] = $_POST['domain']['uuid'];
}
// If we are editing a existing VM load it's existing configuration details
$boolNew = true;
$boolRunning = false;
$arrExistingConfig = [];
$strUUID = '';
$strXML = '';
if (!empty($_GET['uuid'])) {
$strUUID = $_GET['uuid'];
$res = $lv->domain_get_name_by_uuid($strUUID);
$dom = $lv->domain_get_info($res);
$boolNew = false;
$boolRunning = ($lv->domain_state_translate($dom['state']) != 'shutoff');
$arrExistingConfig = domain_to_config($strUUID);
$strXML = $lv->domain_get_xml($res);
}
// Active config for this page
$arrConfig = array_replace_recursive($arrConfigDefaults, $arrExistingConfig);
// Add any custom metadata field defaults (e.g. os)
if (empty($arrConfig['template']['os'])) {
$arrConfig['template']['os'] = ($arrConfig['domain']['clock'] == 'localtime' ? 'windows' : 'linux');
}
if (array_key_exists('createvm', $_POST)) {
$arrResponse = ['success' => true];
if (array_key_exists('xmldesc', $_POST)) {
$tmp = $lv->domain_define($_POST['xmldesc'], !empty($config['domain']['xmlstartnow']));
if (!$tmp){
$arrResponse = ['error' => $lv->get_last_error()];
} else {
$lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1);
// create new VM
if ($_POST['createvm']) {
if ($lv->domain_new($_POST)){
// Fire off the vnc popup if available
$dom = $lv->get_domain_by_name($_POST['domain']['name']);
$vncport = $lv->domain_get_vnc_port($dom);
$wsport = $lv->domain_get_ws_port($dom);
if ($vncport > 0) {
$vnc = '/plugins/dynamix.vm.manager/vnc.html?autoconnect=true&host='.$_SERVER['HTTP_HOST'].'&port='.$wsport.'&path=';
$reply['vncurl'] = $vnc;
}
$reply = ['success' => true];
} else {
$tmp = $lv->domain_new($_POST);
if (!$tmp){
$arrResponse = ['error' => $lv->get_last_error()];
} else {
// Fire off the vnc popup if available
$dom = $lv->get_domain_by_name($_POST['domain']['name']);
$vncport = $lv->domain_get_vnc_port($dom);
$wsport = $lv->domain_get_ws_port($dom);
if ($vncport > 0) {
$vnc = '/plugins/dynamix.vm.manager/vnc.html?autoconnect=true&host=' . $_SERVER['HTTP_HOST'] . '&port=' . $wsport . '&path=';
$arrResponse['vncurl'] = $vnc;
}
}
$reply = ['error' => $lv->get_last_error()];
}
echo json_encode($arrResponse);
echo json_encode($reply);
exit;
}
if (array_key_exists('updatevm', $_POST)) {
$dom = $lv->domain_get_domain_by_uuid($_POST['domain']['uuid']);
// update existing VM
if ($_POST['updatevm']) {
$uuid = $_POST['domain']['uuid'];
$dom = $lv->domain_get_domain_by_uuid($uuid);
$oldAutoStart = $lv->domain_get_autostart($dom)==1;
$newAutoStart = $_POST['domain']['autostart']==1;
$strXML = $lv->domain_get_xml($dom);
if ($boolRunning) {
if ($lv->domain_get_state($dom)=='running') {
$arrErrors = [];
$arrExistingConfig = domain_to_config($_POST['domain']['uuid']);
$arrExistingConfig = domain_to_config($uuid);
$arrNewUSBIDs = $_POST['usb'];
// hot-attach any new usb devices
foreach ($arrNewUSBIDs as $strNewUSBID) {
foreach ($arrExistingConfig['usb'] as $arrExistingUSB) {
if ($strNewUSBID == $arrExistingUSB['id']) {
continue 2;
}
if ($strNewUSBID == $arrExistingUSB['id']) continue 2;
}
list($strVendor, $strProduct) = explode(':', $strNewUSBID);
list($strVendor,$strProduct) = explode(':', $strNewUSBID);
// hot-attach usb
file_put_contents('/tmp/hotattach.tmp', "<hostdev mode='subsystem' type='usb'>
<source startupPolicy='optional'>
<vendor id='0x".$strVendor."'/>
<product id='0x".$strProduct."'/>
</source>
</hostdev>");
exec("virsh attach-device " . escapeshellarg($_POST['domain']['uuid']) . " /tmp/hotattach.tmp --live 2>&1", $arrOutput, $intReturnCode);
file_put_contents('/tmp/hotattach.tmp', "<hostdev mode='subsystem' type='usb'><source startupPolicy='optional'><vendor id='0x".$strVendor."'/><product id='0x".$strProduct."'/></source></hostdev>");
exec("virsh attach-device ".escapeshellarg($uuid)." /tmp/hotattach.tmp --live 2>&1", $arrOutput, $intReturnCode);
if ($intReturnCode != 0) {
$arrErrors[] = implode(' ', $arrOutput);
}
@@ -194,89 +152,78 @@
foreach ($arrExistingConfig['usb'] as $arrExistingUSB) {
if (!in_array($arrExistingUSB['id'], $arrNewUSBIDs)) {
list($strVendor, $strProduct) = explode(':', $arrExistingUSB['id']);
file_put_contents('/tmp/hotdetach.tmp', "<hostdev mode='subsystem' type='usb'>
<source startupPolicy='optional'>
<vendor id='0x".$strVendor."'/>
<product id='0x".$strProduct."'/>
</source>
</hostdev>");
exec("virsh detach-device " . escapeshellarg($_POST['domain']['uuid']) . " /tmp/hotdetach.tmp --live 2>&1", $arrOutput, $intReturnCode);
if ($intReturnCode != 0) {
$arrErrors[] = implode(' ', $arrOutput);
}
file_put_contents('/tmp/hotdetach.tmp', "<hostdev mode='subsystem' type='usb'><source startupPolicy='optional'><vendor id='0x".$strVendor."'/><product id='0x".$strProduct."'/></source></hostdev>");
exec("virsh detach-device ".escapeshellarg($uuid)." /tmp/hotdetach.tmp --live 2>&1", $arrOutput, $intReturnCode);
if ($intReturnCode != 0) $arrErrors[] = implode(' ',$arrOutput);
}
}
if (empty($arrErrors)) {
$arrResponse = ['success' => true];
} else {
$arrResponse = ['error' => implode(', ', $arrErrors)];
}
echo json_encode($arrResponse);
$reply = !$arrErrors ? ['success' => true] : ['error' => implode(', ',$arrErrors)];
echo json_encode($reply);
exit;
}
// Backup xml for existing domain in ram
$strOldXML = '';
$boolOldAutoStart = false;
// backup xml for existing domain in ram
if ($dom) {
$strOldXML = $lv->domain_get_xml($dom);
$boolOldAutoStart = $lv->domain_get_autostart($dom);
if (!array_key_exists('xmldesc', $_POST)) {
$strOldName = $lv->domain_get_name($dom);
$strNewName = $_POST['domain']['name'];
if (!empty($strOldName) &&
!empty($strNewName) &&
is_dir($domain_cfg['DOMAINDIR'].$strOldName.'/') &&
!is_dir($domain_cfg['DOMAINDIR'].$strNewName.'/')) {
// mv domain/vmname folder
if (rename($domain_cfg['DOMAINDIR'].$strOldName, $domain_cfg['DOMAINDIR'].$strNewName)) {
// replace all disk paths in xml
foreach ($_POST['disk'] as &$arrDisk) {
if (!empty($arrDisk['new'])) {
$arrDisk['new'] = str_replace($domain_cfg['DOMAINDIR'].$strOldName.'/', $domain_cfg['DOMAINDIR'].$strNewName.'/', $arrDisk['new']);
}
if (!empty($arrDisk['image'])) {
$arrDisk['image'] = str_replace($domain_cfg['DOMAINDIR'].$strOldName.'/', $domain_cfg['DOMAINDIR'].$strNewName.'/', $arrDisk['image']);
}
}
$oldName = $lv->domain_get_name($dom);
$newName = $_POST['domain']['name'];
$oldDir = $domain_cfg['DOMAINDIR'].$oldName;
$newDir = $domain_cfg['DOMAINDIR'].$newdName;
if ($oldName && $newName && is_dir($oldDir) && !is_dir($newDir)) {
// mv domain/vmname folder
if (rename($oldDir, $newDir)) {
// replace all disk paths in xml
foreach ($_POST['disk'] as &$arrDisk) {
if ($arrDisk['new']) $arrDisk['new'] = str_replace($oldDir, $newDir, $arrDisk['new']);
if ($arrDisk['image']) $arrDisk['image'] = str_replace($oldDir, $newDir, $arrDisk['image']);
}
}
}
}
// Remove existing domain
$lv->nvram_backup($_POST['domain']['uuid']);
// construct updated config
$arrExistingConfig = custom::createArray('domain',$strXML);
$arrExistingConfig['metadata']['vmtemplate']['@attributes']['xmlns'] = 'unraid';
$arrExistingConfig['cputune']['vcpupin'] = [];
$arrUpdatedConfig = custom::createArray('domain',$lv->config_to_xml($_POST));
$arrConfig = array_replace_recursive($arrExistingConfig, $arrUpdatedConfig);
$xml = custom::createXML('domain',$arrConfig)->saveXML();
// delete and create the VM
$lv->nvram_backup($uuid);
$lv->domain_undefine($dom);
$lv->nvram_restore($_POST['domain']['uuid']);
// Save new domain
if (array_key_exists('xmldesc', $_POST)) {
$tmp = $lv->domain_define($_POST['xmldesc']);
$lv->nvram_restore($uuid);
$new = $lv->domain_define($xml);
if ($new) {
$lv->domain_set_autostart($new, $newAutoStart);
$reply = ['success' => true];
} else {
$tmp = $lv->domain_new($_POST);
}
if (!$tmp){
$strLastError = $lv->get_last_error();
// Failure -- try to restore existing domain
$tmp = $lv->domain_define($strOldXML);
if ($tmp) $lv->domain_set_autostart($tmp, $boolOldAutoStart);
$arrResponse = ['error' => $strLastError];
} else {
$lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1);
$arrResponse = ['success' => true];
$old = $lv->domain_define($strXML);
if ($old) $lv->domain_set_autostart($old, $oldAutoStart);
$reply = ['error' => $lv->get_last_error()];
}
echo json_encode($arrResponse);
echo json_encode($reply);
exit;
}
if ($_GET['uuid']) {
// edit an existing VM
$dom = $lv->domain_get_domain_by_uuid($_GET['uuid']);
$boolRunning = $lv->domain_get_state($dom)=='running';
$strXML = $lv->domain_get_xml($dom);
$boolNew = false;
$arrConfig = domain_to_config($_GET['uuid']);
} else {
// edit new VM
$boolRunning = false;
$strXML = '';
$boolNew = true;
$arrConfig = $arrConfigDefaults;
}
// Add any custom metadata field defaults (e.g. os)
if (!$arrConfig['template']['os']) {
$arrConfig['template']['os'] = ($arrConfig['domain']['clock']=='localtime' ? 'windows' : 'linux');
}
?>
<link rel="stylesheet" href="/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.css">
+164
View File
@@ -0,0 +1,164 @@
<?PHP
/* Copyright 2005-2018, Lime Technology
* Copyright 2012-2018, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* Modified for unRAID of Array2XML Published February 9, 2016 by Jeetendra Singh
* Modified for unRAID of xmlToArray Published August 23, 2012 by Tamlyn Rhodes
*/
class custom {
private static $xml = null;
private static $encoding = 'UTF-8';
/*
* Initialize the root XML node [optional]
* @param $version
* @param $encoding
* @param $format_output
*/
public static function init($version = '1.0', $encoding = 'UTF-8', $format_output = true) {
self::$xml = new DomDocument($version, $encoding);
self::$xml->formatOutput = $format_output;
self::$encoding = $encoding;
}
/*
* Convert an Array to XML
* @param string $node_name - name of the root node to be converted
* @param array $arr - aray to be converterd
* @return DomDocument
*/
public static function &createXML($node_name, $arr=array()) {
$xml = self::getXMLRoot();
$xml->appendChild(self::convert($node_name, $arr));
self::$xml = null; // clear the xml node in the class for 2nd time use.
return $xml;
}
/*
* Convert an Array to XML
* @param string $node_name - name of the root node to be converted
* @param array $arr - aray to be converterd
* @return DOMNode
*/
private static function &convert($node_name, $arr=array()) {
//print_arr($node_name);
$xml = self::getXMLRoot();
$node = $xml->createElement($node_name);
if (is_array($arr)) {
// get the attributes first.;
if (isset($arr['@attributes'])) {
foreach ($arr['@attributes'] as $key => $value) {
if (!self::isValidTagName($key)) {
throw new Exception("[custom] Illegal character in attribute name. Attribute: $key in node: $node_name");
}
$node->setAttribute($key, self::bool2str($value));
}
unset($arr['@attributes']); //remove the key from the array once done.
}
// check if it has a value stored in @value, if yes store the value and return
// else check if its directly stored as string
if (isset($arr['@value'])) {
$node->appendChild($xml->createTextNode(self::bool2str($arr['@value'])));
unset($arr['@value']); //remove the key from the array once done.
//return from recursion, as a note with value cannot have child nodes.
return $node;
} elseif (isset($arr['@cdata'])) {
$node->appendChild($xml->createCDATASection(self::bool2str($arr['@cdata'])));
unset($arr['@cdata']); //remove the key from the array once done.
//return from recursion, as a note with cdata cannot have child nodes.
return $node;
}
}
//create subnodes using recursion
if (is_array($arr)) {
// recurse to get the node for that key
foreach ($arr as $key=>$value) {
if (!self::isValidTagName($key)) {
throw new Exception("[custom] Illegal character in tag name. Tag: $key in node: $node_name");
}
if (is_array($value) && is_numeric(key($value))) {
// MORE THAN ONE NODE OF ITS KIND;
// if the new array is numeric index, means it is array of nodes of the same kind
// it should follow the parent key name
foreach ($value as $k=>$v) {
$node->appendChild(self::convert($key, $v));
}
} else {
// ONLY ONE NODE OF ITS KIND
$node->appendChild(self::convert($key, $value));
}
unset($arr[$key]); //remove the key from the array once done.
}
}
// after we are done with all the keys in the array (if it is one)
// we check if it has any text value, if yes, append it.
if (!is_array($arr)) {
$node->appendChild($xml->createTextNode(self::bool2str($arr)));
}
return $node;
}
/*
* Get the root XML node, if there isn't one, create it.
*/
private static function getXMLRoot() {
if (empty(self::$xml)) {
self::init();
}
return self::$xml;
}
/*
* Get string representation of boolean value
*/
private static function bool2str($v) {
//convert boolean to text value.
$v = $v === true ? 'true' : $v;
$v = $v === false ? 'false' : $v;
return $v;
}
/*
* Check if the tag name or attribute name contains illegal characters
* Ref: http://www.w3.org/TR/xml/#sec-common-syn
*/
private static function isValidTagName($tag) {
$pattern = '/^[a-z_]+[a-z0-9\:\-\.\_]*[^:]*$/i';
return preg_match($pattern, $tag, $matches) && $matches[0] == $tag;
}
/*
* Convert xml string into array.
*/
public static function &createArray($root, $xmlstring) {
$xml = simplexml_load_string($xmlstring);
return self::XML2Array($xml)[$root];
}
/*
* Custom converter to process both values and attributes in XML nodes
*/
private static function &XML2Array($xml) {
$attributes = [];
foreach ($xml->attributes() as $attributeName => $attribute) {
$attributes['@attributes'][$attributeName] = (string)$attribute;
}
$tags = [];
foreach ($xml->children() as $child) {
$array = self::XML2Array($child);
list($node, $data) = each($array);
if (!isset($tags[$node])) {
$tags[$node] = $data;
} elseif (is_array($tags[$node]) && array_keys($tags[$node])===range(0, count($tags[$node])-1)) {
$tags[$node][] = $data;
} else {
$tags[$node] = array($tags[$node], $data);
}
}
$textContent = [];
$plainText = trim((string)$xml);
if ($plainText !== '') $textContent['@value'] = $plainText;
$properties = $attributes || $tags || ($plainText==='') ? array_merge($attributes, $tags, $textContent) : $plainText;
return array($xml->getName() => $properties);
}
}
?>