From 0935e480f425803db04d5b9d554f96638bb8f610 Mon Sep 17 00:00:00 2001 From: bergware Date: Mon, 13 Aug 2018 18:28:48 +0200 Subject: [PATCH] VMs: preserve XML custom settings in VM conifg updates --- plugins/dynamix.vm.manager/include/VMedit.php | 14 +- .../templates/Custom.form.php | 227 +++++++----------- plugins/dynamix/include/Custom.php | 164 +++++++++++++ 3 files changed, 256 insertions(+), 149 deletions(-) create mode 100644 plugins/dynamix/include/Custom.php diff --git a/plugins/dynamix.vm.manager/include/VMedit.php b/plugins/dynamix.vm.manager/include/VMedit.php index acc75d731..498ee03fc 100644 --- a/plugins/dynamix.vm.manager/include/VMedit.php +++ b/plugins/dynamix.vm.manager/include/VMedit.php @@ -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']; } - ?> diff --git a/plugins/dynamix.vm.manager/templates/Custom.form.php b/plugins/dynamix.vm.manager/templates/Custom.form.php index 076ba6854..5b4167448 100644 --- a/plugins/dynamix.vm.manager/templates/Custom.form.php +++ b/plugins/dynamix.vm.manager/templates/Custom.form.php @@ -1,6 +1,7 @@ '' ] ] - ]; +]; // 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', " - - - - - "); - exec("virsh attach-device " . escapeshellarg($_POST['domain']['uuid']) . " /tmp/hotattach.tmp --live 2>&1", $arrOutput, $intReturnCode); + file_put_contents('/tmp/hotattach.tmp', ""); + 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', " - - - - - "); - 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', ""); + 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'); + } ?> diff --git a/plugins/dynamix/include/Custom.php b/plugins/dynamix/include/Custom.php new file mode 100644 index 000000000..2e76e0831 --- /dev/null +++ b/plugins/dynamix/include/Custom.php @@ -0,0 +1,164 @@ +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); + } +} +?> \ No newline at end of file