/src/vm/node_modules/vmload/vmload-xml.js
JavaScript | 436 lines | 277 code | 43 blank | 116 comment | 64 complexity | be9e21b9440ad8feb8bc0f24247feb5e MD5 | raw file
- /*
- * CDDL HEADER START
- *
- * The contents of this file are subject to the terms of the
- * Common Development and Distribution License, Version 1.0 only
- * (the "License"). You may not use this file except in compliance
- * with the License.
- *
- * You can obtain a copy of the license at http://smartos.org/CDDL
- *
- * See the License for the specific language governing permissions
- * and limitations under the License.
- *
- * When distributing Covered Code, include this CDDL HEADER in each
- * file.
- *
- * If applicable, add the following below this CDDL HEADER, with the
- * fields enclosed by brackets "[]" replaced with your own identifying
- * information: Portions Copyright [yyyy] [name of copyright owner]
- *
- * CDDL HEADER END
- *
- * Copyright (c) 2014, Joyent, Inc. All rights reserved.
- *
- *
- * vmload-xml: loads VM properties from /etc/zones/<zonename>.xml
- *
- * This is a subcomponent of vmload. The purpose of this piece is to convert
- * the values from the /etc/zones/<zonename>.xml file for a given VM into
- * a JSON object that serves as a building-block of a VM JSON object. Fields
- * are translated to their appropriate data types as defined in proptable.js.
- *
- */
- // Ensure we're using the expected node
- // XXX TODO update this when it's been moved.
- // require('/usr/vm/node/node_modules/platform_node_version').assert();
- var assert = require('assert');
- // XXX TODO update this when it's been moved
- // var expat = require('/usr/vm/node/node_modules/node-expat');
- var expat = require('/usr/node/node_modules/node-expat');
- var fs = require('fs');
- var props = require('/usr/vm/node_modules/props');
- var rtrim = require('utils').rtrim;
- // load generated tables of data
- var BRAND_OPTIONS = props.BRAND_OPTIONS;
- var XML_PROPERTIES = props.XML_PROPERTIES;
- /*
- * endElement() is called when we get to the </element>
- */
- function endElement(name, state)
- {
- // trim stack back above this element
- var stack = state.stack;
- while (stack.pop() !== name) {
- // do nothing, we just want to consume.
- continue;
- }
- }
- /*
- * This function parses the zone XML (string) from 'data' and adds the VM
- * properties to a new object. Upon completion it calls:
- *
- * callback(null, <object>)
- *
- * If an error occurs parsing the XML from 'data', this will call:
- *
- * callback(error)
- *
- * with an Error object having an error string and one of the following as
- * error.code:
- *
- * 'PARSE_ERROR' - error parsing XML
- *
- */
- function getVmobjXML(data, options, callback)
- {
- var allowed;
- var brand_options;
- var err;
- var fields;
- var log;
- var obj = {};
- var parser;
- var state = {};
- assert(options.log, 'no logger passed to getVmobj()');
- log = options.log;
- parser = new expat.Parser('UTF-8');
- state.obj = obj;
- parser.on('startElement', function (name, attrs) {
- startElement(name, attrs, state, log);
- return;
- });
- parser.on('endElement', function (name) {
- endElement(name, state);
- return;
- });
- if (!parser.parse(data)) {
- err = new Error(parser.getError());
- err.code = 'PARSE_ERROR';
- callback(err);
- return;
- }
- /*
- * Ensure we know how to handle this brand, if not someone needs to change
- * proptable.js and rebuild props.js
- */
- assert(obj.brand && (typeof (obj.brand) === 'string'), 'VM missing brand: '
- + JSON.stringify(obj));
- if (BRAND_OPTIONS.hasOwnProperty(obj.brand)) {
- brand_options = BRAND_OPTIONS[obj.brand];
- } else {
- log.warn('VM has unsupported brand: ' + JSON.stringify(obj)
- + ' treating as "joyent"');
- brand_options = BRAND_OPTIONS['joyent'];
- }
- assert(brand_options.hasOwnProperty('allowed_properties'),
- 'Missing list of allowed properties for brand: ' + obj.brand);
- allowed = brand_options.allowed_properties;
- // add in fields we want even if they're empty
- if (!obj.hasOwnProperty('nics')) {
- obj.nics = [];
- }
- if (!obj.hasOwnProperty('disks') && allowed.hasOwnProperty('disks')) {
- obj.disks = [];
- }
- // If this brand uses 'vm_autoboot' instead of autoboot, swap them now.
- if (brand_options.features.use_vm_autoboot) {
- obj.autoboot = obj.vm_autoboot;
- delete obj.vm_autoboot;
- }
- // transitions are virtual properties which spill over to other fields.
- if (obj.hasOwnProperty('transition')) {
- fields = rtrim(obj.transition).split(':');
- if (fields.length === 3) {
- delete obj.transition;
- obj.state = fields[0];
- obj.transition_to = fields[1];
- obj.transition_expire = Number(fields[2]);
- } else {
- log.debug('getVmobj() ignoring bad value for '
- + 'transition "' + obj.transition + '"');
- }
- }
- // sort the disks + nics + filesystems by index
- if (obj.hasOwnProperty('disks')) {
- indexSort(obj.disks, 'path', /^.*-disk(\d+)$/);
- }
- if (obj.hasOwnProperty('nics')) {
- indexSort(obj.nics, 'interface', /^net(\d+)$/);
- }
- if (obj.hasOwnProperty('filesystems')) {
- indexSort(obj.filesystems, 'target', /^(.*)$/);
- }
- callback(null, obj);
- }
- /*
- * This function reads the file 'filename' and passes the data to getVmobjXML().
- * The callback() function will be called in the same manner as with getVmobjXML
- * with the possibility of any errors resulting from fs.readFile().
- *
- */
- function getVmobjXMLFile(filename, options, callback)
- {
- var log;
- assert(filename, 'no filename passed to getVmobj()');
- assert(options.log, 'no logger passed to getVmobj()');
- log = options.log;
- log.trace('reading zone XML from ' + filename);
- fs.readFile(filename, function (error, data) {
- if (error) {
- if (!error.code) {
- error.code = 'READ_FILE';
- }
- callback(error);
- return;
- }
- getVmobjXML(data.toString(), options, callback);
- return;
- });
- }
- /*
- * Sort an array of objects by the given field (ascending)
- */
- function indexSort(obj, field, pattern)
- {
- obj.sort(function (a, b) {
- var avalue = 0;
- var bvalue = 0;
- var matches;
- if (a.hasOwnProperty(field)) {
- matches = a[field].match(pattern);
- if (matches) {
- avalue = Number(matches[1]);
- }
- }
- if (b.hasOwnProperty(field)) {
- matches = b[field].match(pattern);
- if (matches) {
- bvalue = Number(matches[1]);
- }
- }
- return avalue - bvalue;
- });
- }
- /*
- * This is called by the parser when we get to the start of an element. We do
- * all the work here and just allow endElement to pop us back up the stack
- * we're maintaining every time an element starts.
- *
- * In proptable.js we define those elements we'll parse here using entries like:
- *
- * alias: {
- * zonexml: 'zone.attr.alias',
- * loadValueTranslator: 'utils.unbase64'
- * }
- *
- * which means when we get to the part of the XML that looks something like:
- *
- * <zone>
- * ...
- * <attr name="alias" type="string" value="STRING"/>
- *
- * we'll take STRING, run it through the utils.unbase64() function and use the
- * result as the value of 'alias' in our new VM object.
- *
- */
- function startElement(name, attrs, state, log)
- {
- var disk;
- var filesystem;
- var k;
- var nic;
- var obj;
- var stack;
- var transformed;
- var value;
- var where;
- assert(log, 'no logger passed to startElement()');
- if (!state.hasOwnProperty('stack')) {
- state.stack = [];
- }
- obj = state.obj;
- stack = state.stack;
- stack.push(name);
- where = stack.join('.');
- if (where === 'zone') {
- Object.keys(attrs).forEach(function (key) {
- k = where + '.' + key;
- transformed = transformProperty(k, attrs[key], log);
- if (!transformed) {
- return;
- }
- obj[transformed.key] = transformed.value;
- });
- } else if (where === 'zone.attr') {
- k = where + '.' + attrs.name;
- transformed = transformProperty(k, attrs.value, log);
- if (!transformed) {
- return;
- }
- obj[transformed.key] = transformed.value;
- } else if (where === 'zone.dataset') {
- /*
- * zone.dataset is special in that attrs has only a 'name' and we're
- * just making an array of the names, so we don't pass through
- * transformProperty() like the others.
- */
- if (!obj.datasets) {
- obj.datasets = [];
- }
- obj.datasets.push(attrs.name);
- } else if (where === 'zone.device') {
- disk = {};
- Object.keys(attrs).forEach(function (key) {
- k = stack.join('.') + '.' + key;
- transformed = transformProperty(k, attrs[key], log);
- if (!transformed) {
- return;
- }
- disk[transformed.key] = transformed.value;
- });
- if (!obj.disks) {
- obj.disks = [];
- }
- obj.disks.push(disk);
- } else if (where === 'zone.device.net-attr') {
- disk = obj.disks[obj.disks.length - 1];
- k = where + '.' + attrs.name;
- transformed = transformProperty(k, attrs.value, log);
- if (!transformed) {
- return;
- }
- disk[transformed.key] = transformed.value;
- } else if (where === 'zone.filesystem') {
- filesystem = {};
- Object.keys(attrs).forEach(function (key) {
- k = stack.join('.') + '.' + key;
- transformed = transformProperty(k, attrs[key], log);
- if (!transformed) {
- return;
- }
- filesystem[transformed.key] = transformed.value;
- });
- if (!obj.filesystems) {
- obj.filesystems = [];
- }
- obj.filesystems.push(filesystem);
- } else if (where === 'zone.filesystem.fsoption') {
- /*
- * filesystem.fsoptions is special in that attrs has only a 'name' and
- * we're just making an array of the names, so we don't pass through
- * transformProperty() like the others.
- */
- filesystem = obj.filesystems[obj.filesystems.length - 1];
- if (!filesystem.options) {
- filesystem.options = [];
- }
- filesystem.options.push(attrs.name);
- } else if (where === 'zone.network') {
- nic = {};
- Object.keys(attrs).forEach(function (key) {
- k = stack.join('.') + '.' + key;
- transformed = transformProperty(k, attrs[key], log);
- if (!transformed) {
- return;
- }
- nic[transformed.key] = transformed.value;
- });
- if (!obj.nics) {
- obj.nics = [];
- }
- obj.nics.push(nic);
- } else if (where === 'zone.network.net-attr') {
- nic = obj.nics[obj.nics.length - 1];
- k = where + '.' + attrs.name;
- transformed = transformProperty(k, attrs.value, log);
- if (!transformed) {
- return;
- }
- nic[transformed.key] = transformed.value;
- } else if (where === 'zone.rctl') {
- stack.push(attrs.name);
- } else if ((value = where.match(/^(zone.rctl.zone.*.)rctl-value$/))) {
- k = value[1] + attrs.priv + '.' + attrs.action;
- transformed = transformProperty(k, attrs.limit, log);
- if (!transformed) {
- return;
- }
- obj[transformed.key] = transformed.value;
- } else {
- log.error({where: where, attrs: attrs}, 'unhandled zone XML property');
- }
- }
- function transformProperty(key, value, log)
- {
- var result = {};
- log.trace({
- key: {name: key, type: typeof (key)},
- value: {name: value, type: typeof (value)}
- }, 'before transformProperty()');
- if (!XML_PROPERTIES.hasOwnProperty(key)) {
- // missing from XML_PROPERTIES, add to proptable.js / properties
- log.warn('"' + key + '" not found in XML_PROPERTIES');
- return null;
- }
- if (XML_PROPERTIES[key].ignore) {
- log.trace('ignoring "' + key + '" due to "ignore" XML_PROPERTIES');
- return null;
- }
- /*
- * If we're missing objname, that means there was an error building props.js
- * and you most likely need to fix expander.js. All entries in properties
- * in proptable.js should have their keys as the value for 'objname' in
- * props.js.
- */
- assert(XML_PROPERTIES[key].hasOwnProperty('objname'), 'corrupt props.js, '
- + ' XML_PROPERTIES missing "objname" for ' + key);
- result.key = XML_PROPERTIES[key].objname;
- if (XML_PROPERTIES[key].hasOwnProperty('loadValueTranslator')) {
- result.value = XML_PROPERTIES[key].loadValueTranslator(value);
- } else {
- result.value = value;
- }
- log.trace({
- key: {name: result.key, type: typeof (result.key)},
- value: {name: result.value, type: typeof (result.value)}
- }, 'after transformProperty()');
- return result;
- }
- module.exports = {
- getVmobjXML: getVmobjXML,
- getVmobjXMLFile: getVmobjXMLFile
- };