1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License, Version 1.0 only 6 * (the "License"). You may not use this file except in compliance 7 * with the License. 8 * 9 * You can obtain a copy of the license at http://smartos.org/CDDL 10 * 11 * See the License for the specific language governing permissions 12 * and limitations under the License. 13 * 14 * When distributing Covered Code, include this CDDL HEADER in each 15 * file. 16 * 17 * If applicable, add the following below this CDDL HEADER, with the 18 * fields enclosed by brackets "[]" replaced with your own identifying 19 * information: Portions Copyright [yyyy] [name of copyright owner] 20 * 21 * CDDL HEADER END 22 * 23 * Copyright (c) 2013, Joyent, Inc. All rights reserved. 24 * 25 * Experimental functions, expect these interfaces to be unstable and 26 * potentially go away entirely: 27 * 28 * create_snapshot(uuid, snapname, options, callback) 29 * delete_snapshot(uuid, snapname, options, callback) 30 * install(uuid, callback) 31 * receive(target, options, callback) 32 * reprovision(uuid, payload, options, callback) 33 * rollback_snapshot(uuid, snapname, options, callback) 34 * send(uuid, where, options, callback) 35 * getSysinfo(args, callback) 36 * validate(brand, action, payload, callback) 37 * waitForZoneState(payload, state, options, callback) 38 * 39 * Exported functions: 40 * 41 * console(uuid, callback) 42 * create(properties, callback) 43 * delete(uuid, callback) 44 * flatten(vmobj, key) 45 * info(uuid, types, callback) 46 * load([zonename|uuid], callback) 47 * lookup(match, callback) 48 * reboot(uuid, options={[force=true]}, callback) 49 * start(uuid, extra, callback) 50 * stop(uuid, options={[force=true]}, callback) 51 * sysrq(uuid, req=[nmi|screenshot], options={}, callback) 52 * update(uuid, properties, callback) 53 * 54 * Exported variables: 55 * 56 * logname - you can set this to a string [a-zA-Z_] to use as log name 57 * logger - you can set this to a node-bunyan log stream to capture the logs 58 * INFO_TYPES - list of supported types for the info command 59 * SYSRQ_TYPES - list of supported requests for sysrq 60 */ 61 62 // Ensure we're using the platform's node 63 require('/usr/node/node_modules/platform_node_version').assert(); 64 65 var assert = require('assert'); 66 var async = require('/usr/node/node_modules/async'); 67 var bunyan = require('/usr/node/node_modules/bunyan'); 68 var cp = require('child_process'); 69 var dladm = require('/usr/vm/node_modules/dladm'); 70 var EventEmitter = require('events').EventEmitter; 71 var exec = cp.exec; 72 var execFile = cp.execFile; 73 var expat = require('/usr/node/node_modules/node-expat'); 74 var fs = require('fs'); 75 var fw = require('/usr/fw/lib/fw'); 76 var http = require('http'); 77 var net = require('net'); 78 var path = require('path'); 79 var Qmp = require('/usr/vm/node_modules/qmp').Qmp; 80 var spawn = cp.spawn; 81 var sprintf = require('/usr/node/node_modules/sprintf').sprintf; 82 var tty = require('tty'); 83 var util = require('util'); 84 85 var log_to_file = false; 86 87 // keep the last 512 messages just in case we end up wanting them. 88 var ringbuffer = new bunyan.RingBuffer({ limit: 512 }); 89 90 // zfs_list_queue variables for the serialization of 'zfs list' calls 91 var zfs_list_in_progress = {}; 92 var zfs_list_queue; 93 94 // global handle for the zoneevent watcher 95 var zoneevent; 96 97 /* 98 * zone states from libzonecfg/common/zonecfg_impl.h 99 * 100 * #define ZONE_STATE_STR_CONFIGURED "configured" 101 * #define ZONE_STATE_STR_INCOMPLETE "incomplete" 102 * #define ZONE_STATE_STR_INSTALLED "installed" 103 * #define ZONE_STATE_STR_READY "ready" 104 * #define ZONE_STATE_STR_MOUNTED "mounted" 105 * #define ZONE_STATE_STR_RUNNING "running" 106 * #define ZONE_STATE_STR_SHUTTING_DOWN "shutting_down" 107 * #define ZONE_STATE_STR_DOWN "down" 108 * 109 */ 110 111 exports.FLATTENABLE_ARRAYS = [ 112 'resolvers' 113 ]; 114 exports.FLATTENABLE_ARRAY_HASH_KEYS = [ 115 'disks', 116 'nics' 117 ]; 118 exports.FLATTENABLE_HASH_KEYS = [ 119 'customer_metadata', 120 'internal_metadata', 121 'routes', 122 'tags' 123 ]; 124 125 var DEFAULT_MDATA_TIMEOUT = 300; 126 var DISABLED = 0; 127 var MAX_SNAPNAME_LENGTH = 64; 128 var MINIMUM_MAX_SWAP = 256; 129 var PROVISION_TIMEOUT = 300; 130 var STOP_TIMEOUT = 60; 131 var VM = this; 132 133 VM.log = null; 134 135 // can be (re)set by loader before we start. 136 exports.logger = null; 137 exports.loglevel = 'debug'; 138 139 // OpenOnErrorFileStream is a bunyan stream that only creates the file when 140 // there's an error or higher level message or when the global log_to_file 141 // variable is set. For actions that modify things log_to_file is always set. 142 // For other actions we shouldn't log in the normal case but where we do want 143 // logs when something breaks. Thanks to Trent++ for most of this code. 144 // 145 // Note: if you want to rotate the logs while this is writing to a file, you 146 // can first move it. The watcher will notice that the log file was moved and 147 // reopen a new file with the original name. 148 149 function OpenOnErrorFileStream(filename) { 150 this.path = filename; 151 this.write = this.constructor.prototype.write1; 152 this.end = this.constructor.prototype.end1; 153 this.emit = this.constructor.prototype.emit1; 154 this.once = this.constructor.prototype.once1; 155 156 this.newStream = function () { 157 var self = this; 158 var watcher; 159 160 self.stream = fs.createWriteStream(self.path, 161 {flags: 'a', encoding: 'utf8'}); 162 163 watcher = fs.watch(self.path, {persistent: false}, function (evt) { 164 if (evt != 'rename') { 165 return; 166 } 167 // file was renamed, we want to reopen. 168 if (self.stream) { 169 self.stream.destroySoon(); 170 } 171 watcher.close(); 172 self.stream = null; 173 }); 174 }; 175 } 176 177 OpenOnErrorFileStream.prototype.end1 = function () { 178 // in initial mode we're not writing anything, so nothing to flush 179 return; 180 }; 181 182 OpenOnErrorFileStream.prototype.emit1 = function () { 183 return; 184 }; 185 186 // Warning: never emits anything 187 OpenOnErrorFileStream.prototype.once1 = function () { 188 return; 189 }; 190 191 // used until first ERROR or higher, then opens file and ensures future writes 192 // go to .write2() 193 OpenOnErrorFileStream.prototype.write1 = function (rec) { 194 var r; 195 var stream; 196 197 if (rec.level >= bunyan.ERROR || log_to_file) { 198 if (! this.stream) { 199 this.newStream(); 200 } 201 202 stream = this.stream; 203 204 this.emit = function () { stream.emit.apply(stream, arguments); }; 205 this.end = function () { stream.end.apply(stream, arguments); }; 206 this.once = function () { stream.once.apply(stream, arguments); }; 207 this.write = this.constructor.prototype.write2; 208 // dump out logs from ringbuffer too since there was an error so we can 209 // figure out what's going on. 210 for (r in ringbuffer.records) { 211 r = ringbuffer.records[r]; 212 if (r != rec) { 213 this.write(r); 214 } 215 } 216 217 this.write(rec); 218 } 219 220 // This write doesn't fail (since it's going to memory or nowhere) so we 221 // always return true so that callers don't try to wait for 'drain' which 222 // we'll not emit. 223 return true; 224 }; 225 226 // used when writing to file 227 OpenOnErrorFileStream.prototype.write2 = function (rec) { 228 var str; 229 230 // need to support writing '' so we know when to drain 231 if (typeof (rec) === 'string' && rec.length < 1) { 232 str = ''; 233 } else { 234 str = JSON.stringify(rec, bunyan.safeCycles()) + '\n'; 235 } 236 237 if (! this.stream) { 238 this.newStream(); 239 } 240 241 return this.stream.write(str); 242 }; 243 244 // This function should be called by any exported function from this module. 245 // It ensures that a logger is setup. If side_effects is true, we'll start 246 // writing log messages to the file right away. If not, we'll only start 247 // logging after we hit a message error or higher. This is intended such that 248 // things that are expected to change the state or modify VMs on the system: 249 // eg. create, start, stop, delete should have this set true. It should be 250 // set false when the action should not cause changes to the system: 251 // eg.: load, lookup, info, console, &c. 252 function ensureLogging(side_effects) 253 { 254 side_effects = !!side_effects; // make it boolean (undef === false) 255 256 var filename; 257 var logname; 258 var streams = []; 259 260 function start_logging() { 261 var params = { 262 name: logname, 263 streams: streams, 264 serializers: bunyan.stdSerializers 265 }; 266 267 if (process.env.REQ_ID) { 268 params.req_id = process.env.REQ_ID; 269 } else if (process.env.req_id) { 270 params.req_id = process.env.req_id; 271 } 272 VM.log = bunyan.createLogger(params); 273 } 274 275 // This is here in case an app calls a lookup first and then a create. The 276 // logger will get created in no-sideeffects mode for the lookup but when 277 // the create is called this will force the switch to writing. 278 if (side_effects) { 279 log_to_file = true; 280 } 281 282 if (VM.log) { 283 // We're already logging, don't break things. 284 return; 285 } 286 287 if (VM.hasOwnProperty('logname')) { 288 logname = VM.logname.replace(/[^a-zA-Z\_]/g, ''); 289 } 290 if (!logname || logname.length < 1) { 291 logname = 'VM'; 292 } 293 294 if (VM.hasOwnProperty('logger') && VM.logger) { 295 // Use concat, in case someone's sneaky and makes more than one logger. 296 // We don't officially support that yet though. 297 streams = streams.concat(VM.logger); 298 } 299 300 // Add the ringbuffer which we'll dump if we switch from not writing to 301 // writing, and so that they'll show up in dumps. 302 streams.push({ 303 level: 'trace', 304 type: 'raw', 305 stream: ringbuffer 306 }); 307 308 try { 309 if (!fs.existsSync('/var/log/vm')) { 310 fs.mkdirSync('/var/log/vm'); 311 } 312 if (!fs.existsSync('/var/log/vm/logs')) { 313 fs.mkdirSync('/var/log/vm/logs'); 314 } 315 } catch (e) { 316 // We can't ever log to a file in /var/log/vm/logs if we can't create 317 // it, so we just log to ring buffer (above). 318 start_logging(); 319 return; 320 } 321 322 filename = '/var/log/vm/logs/' + Date.now(0) + '-' 323 + sprintf('%06d', process.pid) + '-' + logname + '.log'; 324 325 streams.push({ 326 type: 'raw', 327 stream: new OpenOnErrorFileStream(filename), 328 level: VM.loglevel 329 }); 330 331 start_logging(); 332 } 333 334 function ltrim(str, chars) 335 { 336 chars = chars || '\\s'; 337 str = str || ''; 338 return str.replace(new RegExp('^[' + chars + ']+', 'g'), ''); 339 } 340 341 function rtrim(str, chars) 342 { 343 chars = chars || '\\s'; 344 str = str || ''; 345 return str.replace(new RegExp('[' + chars + ']+$', 'g'), ''); 346 } 347 348 function trim(str, chars) 349 { 350 return ltrim(rtrim(str, chars), chars); 351 } 352 353 function isUUID(str) { 354 var re = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; 355 if (str && str.length === 36 && str.match(re)) { 356 return true; 357 } else { 358 return false; 359 } 360 } 361 362 function fixBoolean(str) 363 { 364 if (str === 'true') { 365 return true; 366 } else if (str === 'false') { 367 return false; 368 } else { 369 return str; 370 } 371 } 372 373 function fixBooleanLoose(str) 374 { 375 if (str === 'true' || str === '1' || str === 1) { 376 return true; 377 } else if (str === 'false' || str === '0' || str === 0) { 378 return false; 379 } else { 380 return str; 381 } 382 } 383 384 function isCIDR(str) { 385 if (typeof (str) !== 'string') { 386 return false; 387 } 388 var parts = str.split('/'); 389 if (parts.length !== 2 || !net.isIPv4(parts[0])) { 390 return false; 391 } 392 393 var size = Number(parts[1]); 394 if (!size || size < 8 || size > 32) { 395 return false; 396 } 397 398 return true; 399 } 400 401 // IMPORTANT: 402 // 403 // Some of these properties get translated below into backward compatible 404 // names. 405 // 406 407 var UPDATABLE_NIC_PROPS = [ 408 'primary', 409 'nic_tag', 410 'vrrp_vrid', 411 'vrrp_primary_ip', 412 'blocked_outgoing_ports', 413 'mac', 414 'gateway', 415 'ip', 416 'model', 417 'netmask', 418 'network_uuid', 419 'dhcp_server', 420 'allow_dhcp_spoofing', 421 'allow_ip_spoofing', 422 'allow_mac_spoofing', 423 'allow_restricted_traffic', 424 'allow_unfiltered_promisc', 425 'vlan_id' 426 ]; 427 428 var UPDATABLE_DISK_PROPS = [ 429 'boot', 430 'model' 431 ]; 432 433 // Note: this doesn't include 'state' because of 'stopping' which is a virtual 434 // state and therefore lookups would be wrong (because they'd search on real 435 // state). 436 var QUICK_LOOKUP = [ 437 'zoneid', 438 'zonename', 439 'zonepath', 440 'uuid', 441 'brand', 442 'ip_type' 443 ]; 444 445 exports.DISK_MODELS = [ 446 'virtio', 447 'ide', 448 'scsi' 449 ]; 450 451 exports.VGA_TYPES = [ 452 'cirrus', 453 'std', 454 'vmware', 455 'qxl', 456 'xenfb' 457 ]; 458 459 exports.INFO_TYPES = [ 460 'all', 461 'block', 462 'blockstats', 463 'chardev', 464 'cpus', 465 'kvm', 466 'pci', 467 'spice', 468 'status', 469 'version', 470 'vnc' 471 ]; 472 473 exports.SYSRQ_TYPES = [ 474 'nmi', 475 'screenshot' 476 ]; 477 478 exports.COMPRESSION_TYPES = [ 479 'on', 480 'off', 481 'lzjb', 482 'gzip', 483 'gzip-1', 484 'gzip-2', 485 'gzip-3', 486 'gzip-4', 487 'gzip-5', 488 'gzip-6', 489 'gzip-7', 490 'gzip-8', 491 'gzip-9', 492 'zle' 493 ]; 494 495 exports.KVM_MEM_OVERHEAD = 1024; 496 exports.KVM_MIN_MEM_OVERHEAD = 256; 497 498 var XML_PROPERTIES = { 499 'zone': { 500 'name': 'zonename', 501 'zonepath': 'zonepath', 502 'autoboot': 'autoboot', 503 'brand': 'brand', 504 'limitpriv': 'limit_priv', 505 'fs-allowed': 'fs_allowed' 506 }, 507 'zone.attr': { 508 'alias': 'alias', 509 'archive-on-delete': 'archive_on_delete', 510 'billing-id': 'billing_id', 511 'boot': 'boot', 512 'cpu-type': 'cpu_type', 513 'create-timestamp': 'create_timestamp', 514 'dataset-uuid': 'image_uuid', 515 'default-gateway': 'default_gateway', 516 'disk-driver': 'disk_driver', 517 'dns-domain': 'dns_domain', 518 'do-not-inventory': 'do_not_inventory', 519 'failed': 'failed', 520 'firewall-enabled': 'firewall_enabled', 521 'hostname': 'hostname', 522 'init-name': 'init_name', 523 'never-booted': 'never_booted', 524 'nic-driver': 'nic_driver', 525 'owner-uuid': 'owner_uuid', 526 'package-name': 'package_name', 527 'package-version': 'package_version', 528 'qemu-extra-opts': 'qemu_extra_opts', 529 'qemu-opts': 'qemu_opts', 530 'ram': 'ram', 531 'restart-init': 'restart_init', 532 'resolvers': 'resolvers', 533 'spice-opts': 'spice_opts', 534 'spice-password': 'spice_password', 535 'spice-port': 'spice_port', 536 'tmpfs': 'tmpfs', 537 'transition': 'transition', 538 'vcpus': 'vcpus', 539 'vga': 'vga', 540 'virtio-txtimer': 'virtio_txtimer', 541 'virtio-txburst': 'virtio_txburst', 542 'vm-version': 'v', 543 'vm-autoboot': 'vm_autoboot', 544 'vnc-password': 'vnc_password', 545 'vnc-port': 'vnc_port' 546 }, 547 'zone.rctl.zone.cpu-shares.rctl-value': { 548 'limit': 'cpu_shares' 549 }, 550 'zone.rctl.zone.cpu-cap.rctl-value': { 551 'limit': 'cpu_cap' 552 }, 553 'zone.rctl.zone.zfs-io-priority.rctl-value': { 554 'limit': 'zfs_io_priority' 555 }, 556 'zone.rctl.zone.max-lwps.rctl-value': { 557 'limit': 'max_lwps' 558 }, 559 'zone.rctl.zone.max-physical-memory.rctl-value': { 560 'limit': 'max_physical_memory' 561 }, 562 'zone.rctl.zone.max-locked-memory.rctl-value': { 563 'limit': 'max_locked_memory' 564 }, 565 'zone.rctl.zone.max-swap.rctl-value': { 566 'limit': 'max_swap' 567 }, 568 'nic': { 569 'ip': 'ip', 570 'mac-addr': 'mac', 571 'physical': 'interface', 572 'vlan-id': 'vlan_id', 573 'global-nic': 'nic_tag', 574 'dhcp_server': 'dhcp_server', 575 'allow_dhcp_spoofing': 'allow_dhcp_spoofing', 576 'allow_ip_spoofing': 'allow_ip_spoofing', 577 'allow_mac_spoofing': 'allow_mac_spoofing', 578 'allow_restricted_traffic': 'allow_restricted_traffic', 579 'allow_unfiltered_promisc': 'allow_unfiltered_promisc', 580 'allowed_ips': 'allowed_ips', 581 'netmask': 'netmask', 582 'network_uuid': 'network_uuid', 583 'model': 'model', 584 'gateway': 'gateway', 585 'primary': 'primary', 586 'vrrp_vrid': 'vrrp_vrid', 587 'vrrp_primary_ip': 'vrrp_primary_ip', 588 'blocked-outgoing-ports': 'blocked_outgoing_ports' 589 }, 590 'filesystem': { 591 'special': 'source', 592 'directory': 'target', 593 'type': 'type', 594 'raw': 'raw' 595 }, 596 'disk': { 597 'boot': 'boot', 598 'image-size': 'image_size', 599 'image-name': 'image_name', 600 'image-uuid': 'image_uuid', 601 'match': 'path', 602 'media': 'media', 603 'model': 'model', 604 'size': 'size' 605 } 606 }; 607 608 /* 609 * This allows one to define a function that will be run over the values from 610 * the zonecfg at the point where we transform that data into a VM object. 611 * 612 */ 613 var XML_PROPERTY_TRANSFORMS = { 614 'alias': unbase64, 615 'archive_on_delete': fixBoolean, 616 'autoboot': fixBoolean, 617 'cpu_cap': numberify, 618 'cpu_shares': numberify, 619 'disks': { 620 'boot': fixBoolean, 621 'image_size': numberify, 622 'size': numberify 623 }, 624 'do_not_inventory': fixBoolean, 625 'firewall_enabled': fixBoolean, 626 'max_locked_memory': unmangleMem, 627 'max_lwps': numberify, 628 'max_physical_memory': unmangleMem, 629 'max_swap': unmangleMem, 630 'never_booted': fixBoolean, 631 'nics': { 632 'dhcp_server': fixBoolean, 633 'allow_dhcp_spoofing': fixBoolean, 634 'allow_ip_spoofing': fixBoolean, 635 'allow_mac_spoofing': fixBoolean, 636 'allow_restricted_traffic': fixBoolean, 637 'allow_unfiltered_promisc': fixBoolean, 638 'allowed_ips': separateCommas, 639 'primary': fixBooleanLoose, 640 'vrrp_vrid': numberify, 641 'vlan_id': numberify 642 }, 643 'qemu_extra_opts': unbase64, 644 'qemu_opts': unbase64, 645 'ram': numberify, 646 'restart_init': fixBoolean, 647 'resolvers': separateCommas, 648 'spice_password': unbase64, 649 'spice_port': numberify, 650 'spice_opts': unbase64, 651 'tmpfs': numberify, 652 'v': numberify, 653 'vcpus': numberify, 654 'virtio_txburst': numberify, 655 'virtio_txtimer': numberify, 656 'vnc_password': unbase64, 657 'vnc_port': numberify, 658 'zfs_io_priority': numberify, 659 'zoneid': numberify 660 }; 661 662 /* 663 * This defines all of the possible properties that could be in a create/update 664 * payload and their types. Each of the entries are required to have at least 665 * a 'type' property which is one of: 666 * 667 * object-array -- an array of objects 668 * boolean -- true or false 669 * flat-object -- an object that has only string properties 670 * integer -- integers only 671 * list -- Either comma separated or array list of strings 672 * string -- Simple string 673 * uuid -- A standard 00000000-0000-0000-0000-000000000000 type uuid 674 * zpool -- The name of an existing zpool 675 * 676 */ 677 var PAYLOAD_PROPERTIES = { 678 'add_disks': {'type': 'object-array', 'check_as': 'disks'}, 679 'add_nics': {'type': 'object-array', 'check_as': 'nics'}, 680 'alias': {'type': 'string'}, 681 'archive_on_delete': {'type': 'boolean'}, 682 'autoboot': {'type': 'boolean'}, 683 'billing_id': {'type': 'string'}, 684 'boot': {'type': 'string'}, 685 'brand': {'type': 'string'}, 686 'cpu_cap': {'type': 'integer'}, 687 'cpu_shares': {'type': 'integer'}, 688 'cpu_type': {'type': 'string'}, 689 'create_only': {'type': 'boolean'}, 690 'create_timestamp': {'type': 'string'}, 691 'customer_metadata': {'type': 'flat-object'}, 692 'dataset_uuid': {'type': 'uuid'}, 693 'delegate_dataset': {'type': 'boolean'}, 694 'disks': {'type': 'object-array'}, 695 'disks.*.block_size': {'type': 'integer'}, 696 'disks.*.boot': {'type': 'boolean'}, 697 'disks.*.compression': {'type': 'string'}, 698 'disks.*.image_name': {'type': 'string'}, 699 'disks.*.image_size': {'type': 'integer'}, 700 'disks.*.image_uuid': {'type': 'uuid'}, 701 'disks.*.refreservation': {'type': 'integer'}, 702 'disks.*.size': {'type': 'integer'}, 703 'disks.*.media': {'type': 'string'}, 704 'disks.*.model': {'type': 'string'}, 705 'disks.*.nocreate': {'type': 'boolean'}, 706 'disks.*.path': {'type': 'string'}, 707 'disks.*.zpool': {'type': 'zpool'}, 708 'disk_driver': {'type': 'string'}, 709 'do_not_inventory': {'type': 'boolean'}, 710 'dns_domain': {'type': 'string'}, 711 'filesystems': {'type': 'object-array'}, 712 'filesystems.*.type': {'type': 'string'}, 713 'filesystems.*.source': {'type': 'string'}, 714 'filesystems.*.target': {'type': 'string'}, 715 'filesystems.*.raw': {'type': 'string'}, 716 'filesystems.*.options': {'type': 'list'}, 717 'firewall': {'type': 'object'}, 718 'firewall_enabled': {'type': 'boolean'}, 719 'fs_allowed': {'type': 'list'}, 720 'hostname': {'type': 'string'}, 721 'image_uuid': {'type': 'uuid'}, 722 'init_name': {'type': 'string'}, 723 'internal_metadata': {'type': 'flat-object'}, 724 'limit_priv': {'type': 'list'}, 725 'max_locked_memory': {'type': 'integer'}, 726 'max_lwps': {'type': 'integer'}, 727 'max_physical_memory': {'type': 'integer'}, 728 'max_swap': {'type': 'integer'}, 729 'mdata_exec_timeout': {'type': 'integer'}, 730 'nics': {'type': 'object-array'}, 731 'nics.*.allow_dhcp_spoofing': {'type': 'boolean'}, 732 'nics.*.allow_ip_spoofing': {'type': 'boolean'}, 733 'nics.*.allow_mac_spoofing': {'type': 'boolean'}, 734 'nics.*.allow_restricted_traffic': {'type': 'boolean'}, 735 'nics.*.allow_unfiltered_promisc': {'type': 'boolean'}, 736 'nics.*.allowed_ips': {'type': 'list'}, 737 'nics.*.blocked_outgoing_ports': {'type': 'list'}, 738 'nics.*.dhcp_server': {'type': 'boolean'}, 739 'nics.*.gateway': {'type': 'string'}, 740 'nics.*.interface': {'type': 'string'}, 741 'nics.*.ip': {'type': 'string'}, 742 'nics.*.mac': {'type': 'string'}, 743 'nics.*.model': {'type': 'string'}, 744 'nics.*.netmask': {'type': 'string'}, 745 'nics.*.network_uuid': {'type': 'uuid'}, 746 'nics.*.nic_tag': {'type': 'string'}, 747 'nics.*.primary': {'type': 'boolean'}, 748 'nics.*.vrrp_vrid': {'type': 'integer-8bit'}, 749 'nics.*.vrrp_primary_ip': {'type': 'string'}, 750 'nics.*.vlan_id': {'type': 'integer'}, 751 'nic_driver': {'type': 'string'}, 752 'nowait': {'type': 'boolean'}, 753 'owner_uuid': {'type': 'string'}, 754 'package_name': {'type': 'string'}, 755 'package_version': {'type': 'string'}, 756 'qemu_opts': {'type': 'string'}, 757 'qemu_extra_opts': {'type': 'string'}, 758 'quota': {'type': 'integer'}, 759 'ram': {'type': 'integer'}, 760 'remove_customer_metadata': {'type': 'list'}, 761 'remove_disks': {'type': 'list'}, 762 'remove_internal_metadata': {'type': 'list'}, 763 'remove_nics': {'type': 'list'}, 764 'remove_routes': {'type': 'list'}, 765 'remove_tags': {'type': 'list'}, 766 'restart_init': {'type': 'boolean'}, 767 'resolvers': {'type': 'list'}, 768 'routes': {'type': 'flat-object'}, 769 'set_routes': {'type': 'flat-object'}, 770 'set_tags': {'type': 'flat-object'}, 771 'set_customer_metadata': {'type': 'flat-object'}, 772 'set_internal_metadata': {'type': 'flat-object'}, 773 'spice_opts': {'type': 'string'}, 774 'spice_password': {'type': 'string'}, 775 'spice_port': {'type': 'integer'}, 776 'tags': {'type': 'flat-object'}, 777 'tmpfs': {'type': 'integer'}, 778 'transition': {'type': 'flat-object'}, 779 'update_disks': {'type': 'object-array', 'check_as': 'disks'}, 780 'update_nics': {'type': 'object-array', 'check_as': 'nics'}, 781 'uuid': {'type': 'uuid'}, 782 'v': {'type': 'integer'}, 783 'vcpus': {'type': 'integer'}, 784 'vga': {'type': 'string'}, 785 'virtio_txburst': {'type': 'integer'}, 786 'virtio_txtimer': {'type': 'integer'}, 787 'vnc_password': {'type': 'string'}, 788 'vnc_port': {'type': 'integer'}, 789 'zfs_data_compression': {'type': 'string'}, 790 'zfs_data_recsize': {'type': 'integer'}, 791 'zfs_io_priority': {'type': 'integer'}, 792 'zfs_root_compression': {'type': 'string'}, 793 'zfs_root_recsize': {'type': 'integer'}, 794 'zone_dataset_uuid': {'type': 'uuid'}, 795 'zonename': {'type': 'string'}, 796 'zfs_storage_pool_name': {'type': 'zpool'}, 797 'zpool': {'type': 'zpool'} 798 }; 799 800 // shared between 'joyent' and 'joyent-minimal' 801 var joyent_allowed = { 802 'add_nics': ['update'], 803 'alias': ['create', 'receive', 'update'], 804 'archive_on_delete': ['create', 'receive', 'update'], 805 'autoboot': ['create', 'receive', 'update'], 806 'billing_id': ['create', 'receive', 'update'], 807 'brand': ['create', 'receive'], 808 'cpu_cap': ['create', 'receive', 'update'], 809 'cpu_shares': ['create', 'receive', 'update'], 810 'create_only': ['receive'], 811 'create_timestamp': ['receive'], 812 'customer_metadata': ['create', 'receive'], 813 'dataset_uuid': ['create', 'receive'], 814 'delegate_dataset': ['create', 'receive'], 815 'do_not_inventory': ['create', 'receive', 'update'], 816 'dns_domain': ['create', 'receive'], 817 'filesystems': ['create', 'receive'], 818 'filesystems.*.type': ['add'], 819 'filesystems.*.source': ['add'], 820 'filesystems.*.target': ['add'], 821 'filesystems.*.raw': ['add'], 822 'filesystems.*.options': ['add'], 823 'firewall': ['create'], 824 'firewall_enabled': ['create', 'receive', 'update'], 825 'fs_allowed': ['create', 'receive', 'update'], 826 'hostname': ['create', 'receive', 'update'], 827 'image_uuid': ['create', 'receive'], 828 'init_name': ['create', 'receive', 'update'], 829 'internal_metadata': ['create', 'receive'], 830 'limit_priv': ['create', 'receive', 'update'], 831 'max_locked_memory': ['create', 'receive', 'update'], 832 'max_lwps': ['create', 'receive', 'update'], 833 'max_physical_memory': ['create', 'receive', 'update'], 834 'max_swap': ['create', 'receive', 'update'], 835 'mdata_exec_timeout': ['create'], 836 'nics': ['create', 'receive'], 837 'nics.*.allow_dhcp_spoofing': ['add', 'update'], 838 'nics.*.allow_ip_spoofing': ['add', 'update'], 839 'nics.*.allow_mac_spoofing': ['add', 'update'], 840 'nics.*.allow_restricted_traffic': ['add', 'update'], 841 'nics.*.allowed_ips': ['add', 'update'], 842 'nics.*.blocked_outgoing_ports': ['add', 'update'], 843 'nics.*.dhcp_server': ['add', 'update'], 844 'nics.*.gateway': ['add', 'update'], 845 'nics.*.interface': ['add', 'update'], 846 'nics.*.ip': ['add', 'update'], 847 'nics.*.mac': ['add', 'update'], 848 'nics.*.netmask': ['add', 'update'], 849 'nics.*.network_uuid': ['add', 'update'], 850 'nics.*.nic_tag': ['add', 'update'], 851 'nics.*.vrrp_vrid': ['add', 'update'], 852 'nics.*.vrrp_primary_ip': ['add', 'update'], 853 'nics.*.primary': ['add', 'update'], 854 'nics.*.vlan_id': ['add', 'update'], 855 'nowait': ['create', 'receive'], 856 'owner_uuid': ['create', 'receive', 'update'], 857 'package_name': ['create', 'receive', 'update'], 858 'package_version': ['create', 'receive', 'update'], 859 'quota': ['create', 'receive', 'update'], 860 'ram': ['create', 'receive', 'update'], 861 'remove_customer_metadata': ['update'], 862 'remove_internal_metadata': ['update'], 863 'remove_nics': ['update'], 864 'remove_routes': ['update'], 865 'remove_tags': ['update'], 866 'restart_init': ['create', 'receive', 'update'], 867 'resolvers': ['create', 'receive', 'update'], 868 'routes': ['create', 'receive'], 869 'set_customer_metadata': ['update'], 870 'set_internal_metadata': ['update'], 871 'set_routes': ['update'], 872 'set_tags': ['update'], 873 'tags': ['create', 'receive'], 874 'tmpfs': ['create', 'receive', 'update'], 875 'transition': ['receive'], 876 'update_nics': ['update'], 877 'uuid': ['create', 'receive'], 878 'v': ['receive'], 879 'zfs_data_compression': ['create', 'receive', 'update'], 880 'zfs_data_recsize': ['create', 'receive', 'update'], 881 'zfs_io_priority': ['create', 'receive', 'update'], 882 'zfs_root_compression': ['create', 'receive', 'update'], 883 'zfs_root_recsize': ['create', 'receive', 'update'], 884 'zfs_storage_pool_name': ['create', 'receive'], 885 'zonename': ['create', 'receive'], 886 'zpool': ['create', 'receive'] 887 }; 888 889 /* 890 * This defines all of the properties allowed, required and features that a 891 * brand has. For each of the allowed/required properties you have a list of 892 * actions for which this is allowed/required. For properties that are lists 893 * of objects, you can specify the action as 'add' or 'update' for when you're 894 * adding or updating one of those objects. 895 * 896 * Features can currently be one of: 897 * 898 * 'cleanup_dataset' -- (boolean) whether to remove trash before booting 899 * 'default_memory_overhead' -- (integer) memory above 'ram' that's added 900 * 'limit_priv': (list) list of priviledges for this zone (if not 'default') 901 * 'mdata_restart' -- (boolean) whether the brand supports restarting its 902 * mdata:fetch service to update properties in the zone 903 * 'min_memory_overhead' -- (integer) minimum delta between ram + max_physical 904 * 'model_required' -- (boolean) whether a .model is required on nics and disks 905 * 'pid_file' -- (pathname) file containing the PID for zones with one process 906 * 'runtime_info' -- (boolean) whether this zone supports the 'info' command 907 * 'serial_console' -- (boolean) whether this zone uses serial console 908 * 'type' -- the type of the VM (OS or KVM), all brands should include this 909 * 'update_mdata_exec_timeout' (boolean) whether to update mdata:exec timeout 910 * 'update_rctls' (boolean) whether we can update rctls 'live' for this zone 911 * 'use_tmpfs' -- (boolean) whether this type of zone uses tmpfs 912 * 'use_vm_autoboot' -- (boolean) use vm-autoboot instead of autoboot 913 * 'use_vmadmd' -- (boolean) use vmadmd for some actions instead of direct 914 * 'var_svc_provisioning' -- (boolean) whether brand uses /var/svc/provisioning 915 * 'wait_for_hwsetup' -- (boolean) use QMP and provision_success when hwsetup 916 * 'write_zone_netfiles' -- (boolean) write out files like /etc/hostname.net0 917 * 'zlogin_console' -- (boolean) use zlogin -C for console (vs. serial_console) 918 * 'zoneinit' -- (boolean) this brand's setup may be controlled by zoneinit 919 * 920 * All of the keys: 921 * 922 * allowed_properties 923 * required_properties 924 * features 925 * 926 * should be defined for each brand. Even if empty. 927 */ 928 var BRAND_OPTIONS = { 929 'joyent': { 930 'allowed_properties': joyent_allowed, 931 'required_properties': { 932 'brand': ['create', 'receive'], 933 'image_uuid': ['create', 'receive'] 934 }, 'features': { 935 'brand_install_script': '/usr/lib/brand/joyent/jinstall', 936 'cleanup_dataset': true, 937 'mdata_restart': true, 938 'reprovision': true, 939 'type': 'OS', 940 'update_mdata_exec_timeout': true, 941 'update_rctls': true, 942 'use_tmpfs': true, 943 'write_zone_netfiles': true, 944 'zlogin_console': true, 945 'zoneinit': true 946 } 947 }, 'joyent-minimal': { 948 'allowed_properties': joyent_allowed, 949 'required_properties': { 950 'brand': ['create', 'receive'], 951 'image_uuid': ['create', 'receive'] 952 }, 'features': { 953 'brand_install_script': '/usr/lib/brand/joyent-minimal/jinstall', 954 'cleanup_dataset': true, 955 'mdata_restart': true, 956 'reprovision': true, 957 'type': 'OS', 958 'update_mdata_exec_timeout': true, 959 'update_rctls': true, 960 'use_tmpfs': true, 961 'var_svc_provisioning': true, 962 'write_zone_netfiles': true, 963 'zlogin_console': true 964 } 965 }, 'sngl': { 966 'allowed_properties': joyent_allowed, 967 'required_properties': { 968 'brand': ['create', 'receive'], 969 'image_uuid': ['create', 'receive'] 970 }, 'features': { 971 'cleanup_dataset': true, 972 'mdata_restart': true, 973 'type': 'OS', 974 'update_mdata_exec_timeout': true, 975 'update_rctls': true, 976 'use_tmpfs': true, 977 'write_zone_netfiles': true, 978 'zlogin_console': true, 979 'zoneinit': true 980 } 981 }, 'kvm': { 982 'allowed_properties': { 983 'add_disks': ['update'], 984 'add_nics': ['update'], 985 'alias': ['create', 'receive', 'update'], 986 'archive_on_delete': ['create', 'receive', 'update'], 987 'autoboot': ['create', 'receive', 'update'], 988 'billing_id': ['create', 'receive', 'update'], 989 'boot': ['create', 'receive', 'update'], 990 'brand': ['create', 'receive'], 991 'cpu_cap': ['create', 'receive', 'update'], 992 'cpu_shares': ['create', 'receive', 'update'], 993 'cpu_type': ['create', 'receive', 'update'], 994 'create_only': ['receive'], 995 'create_timestamp': ['receive'], 996 'customer_metadata': ['create', 'receive'], 997 'disks': ['create', 'receive'], 998 'disks.*.block_size': ['add'], 999 'disks.*.boot': ['add', 'update'], 1000 'disks.*.compression': ['add', 'update'], 1001 'disks.*.image_name': ['add', 'update'], 1002 'disks.*.image_size': ['add'], 1003 'disks.*.image_uuid': ['add'], 1004 'disks.*.refreservation': ['add', 'update'], 1005 'disks.*.size': ['add'], 1006 'disks.*.media': ['add', 'update'], 1007 'disks.*.model': ['add', 'update'], 1008 'disks.*.nocreate': ['add'], 1009 'disks.*.path': ['add', 'update'], 1010 'disks.*.zpool': ['add'], 1011 'disk_driver': ['create', 'receive', 'update'], 1012 'do_not_inventory': ['create', 'receive', 'update'], 1013 'firewall': ['create'], 1014 'firewall_enabled': ['create', 'receive', 'update'], 1015 'hostname': ['create', 'receive', 'update'], 1016 'image_uuid': ['create', 'receive'], 1017 'internal_metadata': ['create', 'receive'], 1018 'limit_priv': ['create', 'receive', 'update'], 1019 'max_locked_memory': ['create', 'receive', 'update'], 1020 'max_lwps': ['create', 'receive', 'update'], 1021 'max_physical_memory': ['create', 'receive', 'update'], 1022 'max_swap': ['create', 'receive', 'update'], 1023 'nics': ['create', 'receive'], 1024 'nics.*.allow_dhcp_spoofing': ['add', 'update'], 1025 'nics.*.allow_ip_spoofing': ['add', 'update'], 1026 'nics.*.allow_mac_spoofing': ['add', 'update'], 1027 'nics.*.allow_restricted_traffic': ['add', 'update'], 1028 'nics.*.allow_unfiltered_promisc': ['add', 'update'], 1029 'nics.*.allowed_ips': ['add', 'update'], 1030 'nics.*.blocked_outgoing_ports': ['add', 'update'], 1031 'nics.*.dhcp_server': ['add', 'update'], 1032 'nics.*.gateway': ['add', 'update'], 1033 'nics.*.interface': ['add', 'update'], 1034 'nics.*.ip': ['add', 'update'], 1035 'nics.*.mac': ['add', 'update'], 1036 'nics.*.model': ['add', 'update'], 1037 'nics.*.netmask': ['add', 'update'], 1038 'nics.*.network_uuid': ['add', 'update'], 1039 'nics.*.nic_tag': ['add', 'update'], 1040 'nics.*.primary': ['add', 'update'], 1041 'nics.*.vlan_id': ['add', 'update'], 1042 'nic_driver': ['create', 'receive', 'update'], 1043 'owner_uuid': ['create', 'receive', 'update'], 1044 'package_name': ['create', 'receive', 'update'], 1045 'package_version': ['create', 'receive', 'update'], 1046 'qemu_opts': ['create', 'receive', 'update'], 1047 'qemu_extra_opts': ['create', 'receive', 'update'], 1048 'quota': ['create', 'receive', 'update'], 1049 'ram': ['create', 'receive', 'update'], 1050 'remove_customer_metadata': ['update'], 1051 'remove_disks': ['update'], 1052 'remove_internal_metadata': ['update'], 1053 'remove_nics': ['update'], 1054 'remove_routes': ['update'], 1055 'remove_tags': ['update'], 1056 'resolvers': ['create', 'receive', 'update'], 1057 'set_customer_metadata': ['update'], 1058 'set_internal_metadata': ['update'], 1059 'set_routes': ['update'], 1060 'set_tags': ['update'], 1061 'spice_opts': ['create', 'receive', 'update'], 1062 'spice_password': ['create', 'receive', 'update'], 1063 'spice_port': ['create', 'receive', 'update'], 1064 'tags': ['create', 'receive'], 1065 'transition': ['receive'], 1066 'update_disks': ['update'], 1067 'update_nics': ['update'], 1068 'uuid': ['create', 'receive'], 1069 'v': ['receive'], 1070 'vcpus': ['create', 'receive', 'update'], 1071 'vga': ['create', 'receive', 'update'], 1072 'virtio_txburst': ['create', 'receive', 'update'], 1073 'virtio_txtimer': ['create', 'receive', 'update'], 1074 'vnc_password': ['create', 'receive', 'update'], 1075 'vnc_port': ['create', 'receive', 'update'], 1076 'zfs_io_priority': ['create', 'receive', 'update'], 1077 'zfs_root_compression': ['create', 'receive', 'update'], 1078 'zfs_root_recsize': ['create', 'receive', 'update'], 1079 'zone_dataset_uuid': ['create', 'receive'], 1080 'zpool': ['create', 'receive'] 1081 }, 'required_properties': { 1082 'brand': ['create', 'receive'] 1083 }, 'features': { 1084 'default_memory_overhead': VM.KVM_MEM_OVERHEAD, 1085 'limit_priv': ['default', '-file_link_any', '-net_access', 1086 '-proc_fork', '-proc_info', '-proc_session'], 1087 'min_memory_overhead': VM.KVM_MIN_MEM_OVERHEAD, 1088 'model_required': true, 1089 'pid_file': '/tmp/vm.pid', 1090 'runtime_info': true, 1091 'serial_console': true, 1092 'type': 'KVM', 1093 'use_vm_autoboot': true, 1094 'use_vmadmd': true, 1095 'var_svc_provisioning': true, 1096 'wait_for_hwsetup': true 1097 } 1098 } 1099 }; 1100 1101 var VIRTIO_TXTIMER_DEFAULT = 200000; 1102 var VIRTIO_TXBURST_DEFAULT = 128; 1103 1104 function getZpools(log, callback) 1105 { 1106 var args = ['list', '-H', '-p', '-o', 'name']; 1107 var cmd = '/usr/sbin/zpool'; 1108 var idx; 1109 var raw = []; 1110 var zpools = []; 1111 1112 assert(log, 'no logger passed to getZpools()'); 1113 1114 log.debug(cmd + ' ' + args.join(' ')); 1115 execFile(cmd, args, function (error, stdout, stderr) { 1116 if (error) { 1117 log.error('Unable to get list of zpools'); 1118 callback(error, {'stdout': stdout, 'stderr': stderr}); 1119 } else { 1120 // strip out any empty values (last one). 1121 raw = stdout.split('\n'); 1122 for (idx in raw) { 1123 if (raw[idx].length > 0) { 1124 zpools.push(raw[idx]); 1125 } 1126 } 1127 callback(null, zpools); 1128 } 1129 }); 1130 } 1131 1132 /* 1133 * When you need to access files inside a zoneroot, you need to be careful that 1134 * there are no symlinks in the path. Since we operate from the GZ, these 1135 * symlinks will be evaluated in the GZ context. Eg. a symlink in zone A with 1136 * /var/run -> * /zones/<uuid of zone B>/root/var/run would mean that operating 1137 * on files in zone A's /var/run would actually be touching files in zone B. 1138 * 1139 * To prevent that, only ever modify files inside the zoneroot from the GZ 1140 * *before* first boot. After the zone is booted, it's better to use services 1141 * in the zone to pull values from metadata and write out changes on next boot. 1142 * It's also safe to use zlogin when the zone is running. 1143 * 1144 * This function is intended to be used in those cases we do write things out 1145 * before the zone's first boot but the dataset might have invalid symlinks in 1146 * it even then, so we still need to confirm the paths inside zoneroot before 1147 * using them. It throws an exception if: 1148 * 1149 * - zoneroot is not an absolute path 1150 * - fs.lstatSync fails 1151 * - target path under zoneroot contains symlink 1152 * - a component leading up to the final one is not a directory 1153 * - options.type is set to 'file' and target is not a regular file 1154 * - options.type is set to 'dir' and target references a non-directory 1155 * - options.type is not one of 'file' or 'dir' 1156 * - options.enoent_ok is false and target path doesn't exist 1157 * 1158 * if none of those are the case, it returns true. 1159 */ 1160 function assertSafeZonePath(zoneroot, target, options) 1161 { 1162 var parts; 1163 var root; 1164 var stat; 1165 var test; 1166 1167 assert((zoneroot.length > 0 && zoneroot[0] === '/'), 1168 'zoneroot must be an absolute path not: [' + zoneroot + ']'); 1169 1170 parts = trim(target, '/').split('/'); 1171 root = trim(zoneroot, '/'); 1172 test = '/' + root; 1173 1174 while (parts.length > 0) { 1175 test = test + '/' + parts.shift(); 1176 1177 try { 1178 stat = fs.lstatSync(test); 1179 } catch (e) { 1180 if (e.code === 'ENOENT') { 1181 if (!options.hasOwnProperty('enoent_ok') 1182 || options.enoent_ok === false) { 1183 1184 throw e; 1185 } else { 1186 // enoent is ok, return true. This is mostly used when 1187 // deleting files with rm -f <path>. It's ok for <path> to 1188 // not exist (but not ok for any component to be a symlink) 1189 // there's no point continuing though since ENOENT here 1190 // means all subpaths also won't exist. 1191 return true; 1192 } 1193 } else { 1194 throw e; 1195 } 1196 } 1197 1198 if (stat.isSymbolicLink()) { 1199 // it's never ok to have a symlink component 1200 throw new Error(test + ' is a symlink'); 1201 } 1202 1203 // any component other than the last also needs to be a 1204 // directory, last can also be a file. 1205 if (parts.length === 0) { 1206 // last, dir or file 1207 if (!options.hasOwnProperty('type') || options.type === 'dir') { 1208 if (!stat.isDirectory()) { 1209 throw new Error(test + ' is not a directory'); 1210 } 1211 } else if (options.type === 'file') { 1212 if (!stat.isFile()) { 1213 throw new Error(test + ' is not a file'); 1214 } 1215 } else { 1216 throw new Error('this function does not know about ' 1217 + options.type); 1218 } 1219 } else if (!stat.isDirectory()) { 1220 // not last component, only dir is acceptable 1221 throw new Error(test + ' is not a directory'); 1222 } 1223 } 1224 // if we didn't throw, this is valid. 1225 return true; 1226 } 1227 1228 function validateProperty(brand, prop, value, action, data, errors, log) 1229 { 1230 var allowed; 1231 var k; 1232 1233 assert(log, 'no logger passed to validateProperty()'); 1234 1235 if (!data.hasOwnProperty('zpools')) { 1236 data.zpools = []; 1237 } 1238 1239 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')) { 1240 allowed = BRAND_OPTIONS[brand].allowed_properties; 1241 } else { 1242 allowed = {}; 1243 } 1244 1245 if (!errors.hasOwnProperty('bad_values')) { 1246 errors.bad_values = []; 1247 } 1248 if (!errors.hasOwnProperty('bad_properties')) { 1249 errors.bad_properties = []; 1250 } 1251 1252 if (!allowed.hasOwnProperty(prop)) { 1253 // thie BRAND_OPTIONS doesn't have this property at all 1254 if (errors.bad_properties.indexOf(prop) === -1) { 1255 errors.bad_properties.push(prop); 1256 } 1257 } else if (!Array.isArray(allowed[prop]) 1258 || allowed[prop].indexOf(action) === -1) { 1259 1260 // here we've ether got no actions allowed for this value, 1261 // or just not this one 1262 if (errors.bad_properties.indexOf(prop) === -1) { 1263 errors.bad_properties.push(prop); 1264 } 1265 } 1266 1267 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)) { 1268 switch (PAYLOAD_PROPERTIES[prop].type) { 1269 case 'uuid': 1270 if (typeof (value) === 'string' && !isUUID(value) 1271 && errors.bad_values.indexOf(prop) === -1) { 1272 1273 errors.bad_values.push(prop); 1274 } 1275 break; 1276 case 'boolean': 1277 if (value === 1 || value === '1') { 1278 log.warn('DEPRECATED: payload uses 1 instead of ' 1279 + 'true for ' + prop + ', use "true" instead.'); 1280 } else if (typeof (fixBoolean(value)) !== 'boolean' 1281 && errors.bad_values.indexOf(prop) === -1) { 1282 1283 errors.bad_values.push(prop); 1284 } 1285 break; 1286 case 'string': 1287 if (value === undefined || value === null 1288 || trim(value.toString()) === '') { 1289 // if set empty/false we'll keep since this is used to unset 1290 break; 1291 } else if (typeof (value) !== 'string' 1292 && errors.bad_values.indexOf(prop) === -1) { 1293 1294 errors.bad_values.push(prop); 1295 } 1296 break; 1297 case 'integer': 1298 if (value === undefined || value === null 1299 || trim(value.toString()) === '') { 1300 // if set empty/false we'll keep since this is used to unset 1301 break; 1302 } else if (((typeof (value) !== 'string' 1303 && typeof (value) !== 'number') 1304 || !value.toString().match(/^[0-9]+$/)) 1305 && errors.bad_values.indexOf(prop) === -1) { 1306 1307 if ((['vnc_port', 'spice_port'].indexOf(prop) !== -1) 1308 && (value.toString() === '-1')) { 1309 1310 // these keys allow '-1' as a value, so we succeed here even 1311 // though we'd otherwise fail. 1312 break; 1313 } 1314 1315 errors.bad_values.push(prop); 1316 } else if (prop === 'max_swap' && value < MINIMUM_MAX_SWAP) { 1317 errors.bad_values.push(prop); 1318 } 1319 break; 1320 case 'integer-8bit': 1321 if (value === undefined || value === null 1322 || trim(value.toString()) === '') { 1323 // if set empty/false we'll keep since this is used to unset 1324 break; 1325 } else if (((typeof (value) !== 'string' 1326 && typeof (value) !== 'number') 1327 || !value.toString().match(/^[0-9]+$/)) 1328 && errors.bad_values.indexOf(prop) === -1 1329 ) { 1330 1331 errors.bad_values.push(prop); 1332 break; 1333 } 1334 if (value < 0 || value > 255) { 1335 errors.bad_values.push(prop); 1336 } 1337 break; 1338 case 'zpool': 1339 if ((typeof (value) !== 'string' 1340 || data.zpools.indexOf(value) === -1) 1341 && errors.bad_values.indexOf(prop) === -1) { 1342 1343 errors.bad_values.push(prop); 1344 } 1345 break; 1346 case 'object': 1347 if (typeof (value) !== 'object' 1348 && errors.bad_values.indexOf(prop) === -1) { 1349 1350 errors.bad_values.push(prop); 1351 } 1352 break; 1353 case 'flat-object': 1354 if (typeof (value) !== 'object' 1355 && errors.bad_values.indexOf(prop) === -1) { 1356 1357 errors.bad_values.push(prop); 1358 } 1359 for (k in value) { 1360 if (typeof (value[k]) !== 'string' 1361 && typeof (value[k]) !== 'number' 1362 && typeof (value[k]) !== 'boolean') { 1363 1364 if (errors.bad_values.indexOf(prop) === -1) { 1365 errors.bad_values.push(prop); 1366 } 1367 break; 1368 } 1369 } 1370 break; 1371 case 'list': 1372 if (typeof (value) === 'string') { 1373 // really any string could be valid (a one element list) 1374 break; 1375 } else if (Array.isArray(value)) { 1376 for (k in value) { 1377 if (typeof (value[k]) !== 'string' 1378 && typeof (value[k]) !== 'number') { 1379 1380 // TODO: log something more useful here telling them 1381 // the type is invalid. 1382 if (errors.bad_values.indexOf(prop) === -1) { 1383 errors.bad_values.push(prop); 1384 } 1385 break; 1386 } 1387 // if this is an array, it can't have commas in the 1388 // values. (since we might stringify the list and 1389 // we'd end up with something different. 1390 if (value[k].toString().indexOf(',') !== -1 1391 && errors.bad_values.indexOf(prop) === -1) { 1392 1393 errors.bad_values.push(prop); 1394 } 1395 } 1396 } else { 1397 // not a valid type 1398 if (errors.bad_values.indexOf(prop) === -1) { 1399 errors.bad_values.push(prop); 1400 } 1401 } 1402 break; 1403 case 'object-array': 1404 if (!Array.isArray(value)) { 1405 if (errors.bad_values.indexOf(prop) === -1) { 1406 errors.bad_values.push(prop); 1407 } 1408 break; 1409 } 1410 for (k in value) { 1411 if (typeof (value[k]) !== 'object') { 1412 if (errors.bad_values.indexOf(prop) === -1) { 1413 errors.bad_values.push(prop); 1414 } 1415 break; 1416 } 1417 } 1418 break; 1419 default: 1420 // don't know what type of prop this is, so it's invalid 1421 if (errors.bad_properties.indexOf(prop) === -1) { 1422 errors.bad_properties.push(prop); 1423 } 1424 break; 1425 } 1426 } 1427 } 1428 1429 /* 1430 * image properties: 1431 * 1432 * size (optional, only used by zvols) 1433 * type ('zvol' or 'zone-dataset') 1434 * uuid 1435 * zpool 1436 * 1437 */ 1438 function validateImage(image, log, callback) 1439 { 1440 var args; 1441 var cmd = '/usr/sbin/imgadm'; 1442 1443 args = ['get', '-P', image.zpool, image.uuid]; 1444 1445 log.debug(cmd + ' ' + args.join(' ')); 1446 1447 // on any error we fail closed (assume the image does not exist) 1448 execFile(cmd, args, function (error, stdout, stderr) { 1449 var data; 1450 var e; 1451 1452 if (error) { 1453 error.stdout = stdout; 1454 error.stderr = stderr; 1455 error.whatFailed = 'EEXECFILE'; 1456 log.error(error); 1457 callback(error); 1458 return; 1459 } 1460 1461 try { 1462 data = JSON.parse(stdout.toString()); 1463 } catch (err) { 1464 data = {}; 1465 } 1466 1467 if (data.hasOwnProperty('manifest')) { 1468 if (data.manifest.type !== image.type) { 1469 // image is wrong type 1470 e = new Error('image ' + image.uuid + ' is type ' 1471 + data.manifest.type + ', must be ' + image.type); 1472 e.whatFailed = 'EBADTYPE'; 1473 log.error(e); 1474 callback(e); 1475 return; 1476 } 1477 log.info('image ' + image.uuid + ' found in imgadm'); 1478 1479 // If image_size is missing, add it. If it's wrong, error. 1480 if (data.manifest.hasOwnProperty('image_size')) { 1481 if (image.hasOwnProperty('size')) { 1482 if (image.size !== data.manifest.image_size) { 1483 e = new Error('incorrect image_size value for image' 1484 + ' ' + image.uuid + ' passed: ' 1485 + image.size + ' should be: ' 1486 + data.manifest.image_size); 1487 e.whatFailed = 'EBADSIZE'; 1488 log.error(e); 1489 callback(e); 1490 return; 1491 } 1492 } else { 1493 // image doesn't have size, manifest does, add it. 1494 image.size = data.manifest.image_size; 1495 } 1496 } 1497 // everything ok 1498 callback(); 1499 } else { 1500 e = new Error('cannot find \'manifest\' for image ' 1501 + image.uuid); 1502 e.whatFailed = 'ENOENT'; 1503 log.error(e); 1504 callback(e); 1505 return; 1506 } 1507 }); 1508 } 1509 1510 // Ensure if image_uuid is passed either at top level or for disks.*.image_uuid 1511 // that image_uuid exists on the system according to imgadm. 1512 // 1513 // NOTE: if image_size is missing from payload, but found in imgadm it is added 1514 // to the payload here. 1515 // 1516 function validateImages(payload, errors, log, callback) 1517 { 1518 var check_images = []; 1519 var disk_idx; 1520 var pool; 1521 1522 if (payload.hasOwnProperty('image_uuid') && isUUID(payload.image_uuid)) { 1523 if (payload.hasOwnProperty('zpool')) { 1524 pool = payload.zpool; 1525 } else { 1526 pool = 'zones'; 1527 } 1528 1529 check_images.push({ 1530 'property': 'image_uuid', 1531 'target': payload, 1532 'type': 'zone-dataset', 1533 'uuid': payload.image_uuid, 1534 'zpool': pool 1535 }); 1536 } 1537 1538 ['disks', 'add_disks'].forEach(function (d) { 1539 if (payload.hasOwnProperty(d)) { 1540 disk_idx = 0; 1541 payload[d].forEach(function (disk) { 1542 if (disk.hasOwnProperty('image_uuid')) { 1543 if (disk.hasOwnProperty('zpool')) { 1544 pool = disk.zpool; 1545 } else { 1546 pool = 'zones'; 1547 } 1548 check_images.push({ 1549 'property_prefix': d + '.' + disk_idx, 1550 'property': d + '.' + disk_idx + '.image_uuid', 1551 'target': disk, 1552 'type': 'zvol', 1553 'uuid': disk.image_uuid, 1554 'zpool': pool 1555 }); 1556 } 1557 disk_idx++; 1558 }); 1559 } 1560 }); 1561 1562 async.forEachSeries(check_images, function (image, cb) { 1563 1564 var i; 1565 var idx; 1566 1567 i = { 1568 uuid: image.uuid, 1569 type: image.type, 1570 zpool: image.zpool 1571 }; 1572 1573 if (image.target.hasOwnProperty('image_size')) { 1574 i.size = image.target.image_size; 1575 } 1576 1577 validateImage(i, log, function (err) { 1578 if (err) { 1579 switch (err.whatFailed) { 1580 case 'EBADSIZE': 1581 // image.size is wrong (vs. manifest) 1582 errors.bad_values.push(image.property_prefix 1583 + '.image_size'); 1584 break; 1585 case 'ENOENT': 1586 // image.uuid not found in imgadm 1587 errors.bad_values.push(image.property); 1588 break; 1589 case 'EBADTYPE': 1590 // image.type is wrong 1591 errors.bad_values.push(image.property); 1592 break; 1593 default: 1594 // unknown error, fail closed 1595 errors.bad_values.push(image.property); 1596 break; 1597 } 1598 } else { 1599 // no errors, so check if size was added 1600 if (i.hasOwnProperty('size')) { 1601 if (!image.target.hasOwnProperty('image_size')) { 1602 image.target.image_size = i.size; 1603 // Remove error that would have been added earlier 1604 // when we didn't have image_size 1605 idx = errors.missing_properties.indexOf( 1606 image.property_prefix + '.image_size'); 1607 if (idx !== -1) { 1608 errors.missing_properties.splice(idx, 1); 1609 } 1610 } 1611 } 1612 } 1613 1614 cb(); 1615 }); 1616 }, function () { 1617 callback(); 1618 }); 1619 } 1620 1621 // This is for allowed_ips which accepts IPiv4 addresses or CIDR addresses in 1622 // the form IP/MASK where MASK is 1-32. 1623 function validateIPlist(list) { 1624 var invalid = []; 1625 1626 list.forEach(function (ip) { 1627 var matches; 1628 if (!net.isIPv4(ip)) { 1629 matches = ip.match(/^([0-9\.]+)\/([0-9]+)$/); 1630 if (matches && net.isIPv4(matches[1]) 1631 && (Number(matches[2]) >= 1) && (Number(matches[2]) <= 32)) { 1632 1633 // In this case it wasn't an IPv4, but it was a valid CIDR 1634 return; 1635 } else { 1636 invalid.push(ip); 1637 } 1638 } 1639 }); 1640 1641 if (invalid.length !== 0) { 1642 throw new Error('invalid allowed_ips: ' + invalid.join(', ')); 1643 } 1644 1645 if (list.length > 13) { 1646 throw new Error('Maximum of 13 allowed_ips per nic'); 1647 } 1648 } 1649 1650 exports.validate = function (brand, action, payload, options, callback) 1651 { 1652 var errors = { 1653 'bad_values': [], 1654 'bad_properties': [], 1655 'missing_properties': [] 1656 }; 1657 var log; 1658 var prop; 1659 1660 // options is optional 1661 if (arguments.length === 4) { 1662 callback = arguments[3]; 1663 options = {}; 1664 } 1665 1666 ensureLogging(false); 1667 if (options.hasOwnProperty('log')) { 1668 log = options.log; 1669 } else { 1670 log = VM.log.child({action: 'validate'}); 1671 } 1672 1673 if (!BRAND_OPTIONS.hasOwnProperty(brand)) { 1674 if (!brand) { 1675 brand = 'undefined'; 1676 } 1677 callback({'bad_brand': brand}); 1678 return; 1679 } 1680 1681 // wrap the whole thing with getZpools so we have the list of pools if we 1682 // need them. 1683 getZpools(log, function (err, zpools) { 1684 var disk_idx; 1685 var idx; 1686 var prefix; 1687 var required; 1688 var subprop; 1689 var subprop_action = ''; 1690 var value; 1691 1692 if (err) { 1693 /* 1694 * this only happens when the zpool command fails which should be 1695 * very rare, but when it does happen, we continue with an empty 1696 * zpool list in case they don't need to validate zpools. If they 1697 * do, every zpool will be invalid which is also what we want since 1698 * nothing else that uses zpools is likely to work either. 1699 * 1700 */ 1701 zpools = []; 1702 } 1703 1704 // loop through and weed out ones we don't allow for this action. 1705 for (prop in payload) { 1706 validateProperty(brand, prop, payload[prop], action, 1707 {zpools: zpools}, errors, log); 1708 1709 // special case for complex properties where we want to check 1710 // foo.*.whatever 1711 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop) 1712 && PAYLOAD_PROPERTIES[prop].type === 'object-array' 1713 && Array.isArray(payload[prop])) { 1714 1715 if (PAYLOAD_PROPERTIES[prop].hasOwnProperty('check_as')) { 1716 prefix = PAYLOAD_PROPERTIES[prop].check_as + '.*.'; 1717 if (prop.match(/^add_/)) { 1718 subprop_action = 'add'; 1719 } else if (prop.match(/^update_/)) { 1720 subprop_action = 'update'; 1721 } 1722 } else { 1723 // here we've got something like 'disks' which is an add 1724 prefix = prop + '.*.'; 1725 subprop_action = 'add'; 1726 } 1727 1728 for (idx in payload[prop]) { 1729 if (typeof (payload[prop][idx]) === 'object') { 1730 // subprop will be something like 'nic_tag' 1731 for (subprop in payload[prop][idx]) { 1732 value = payload[prop][idx][subprop]; 1733 validateProperty(brand, prefix + subprop, value, 1734 subprop_action, {zpools: zpools}, errors, log); 1735 } 1736 } else if (errors.bad_values.indexOf(prop) === -1) { 1737 // this is not an object so bad value in the array 1738 errors.bad_values.push(prop); 1739 } 1740 } 1741 } 1742 } 1743 1744 // special case: if you have disks you must specify either image_uuid 1745 // and image_size *or* size and block_size is only allowed when you use 1746 // 'size' and image_name when you don't. 1747 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties') 1748 && BRAND_OPTIONS[brand].allowed_properties 1749 .hasOwnProperty('disks')) { 1750 1751 function validateDiskSource(prop_prefix, disk) { 1752 1753 if (disk.hasOwnProperty('media') && disk.media !== 'disk') { 1754 // we only care about disks here, not cdroms. 1755 return; 1756 } 1757 1758 if (disk.hasOwnProperty('image_uuid')) { 1759 // with image_uuid, size is invalid and image_size is 1760 // required, additionally block_size is not allowed. 1761 1762 if (!disk.hasOwnProperty('image_size')) { 1763 errors.missing_properties.push(prop_prefix 1764 + '.image_size'); 1765 } 1766 if (disk.hasOwnProperty('size')) { 1767 errors.bad_properties.push(prop_prefix + '.size'); 1768 } 1769 if (disk.hasOwnProperty('block_size')) { 1770 errors.bad_properties.push(prop_prefix 1771 + '.block_size'); 1772 } 1773 } else { 1774 // without image_uuid, image_size and image_name are invalid 1775 // and 'size' is required. 1776 1777 if (!disk.hasOwnProperty('size')) { 1778 errors.missing_properties.push(prop_prefix + '.size'); 1779 } 1780 if (disk.hasOwnProperty('image_name')) { 1781 errors.bad_properties.push(prop_prefix + '.image_name'); 1782 } 1783 if (disk.hasOwnProperty('image_size')) { 1784 errors.bad_properties.push(prop_prefix + '.image_size'); 1785 } 1786 } 1787 } 1788 1789 if (payload.hasOwnProperty('disks')) { 1790 for (disk_idx in payload.disks) { 1791 validateDiskSource('disks.' + disk_idx, 1792 payload.disks[disk_idx]); 1793 } 1794 } 1795 if (payload.hasOwnProperty('add_disks')) { 1796 for (disk_idx in payload.add_disks) { 1797 validateDiskSource('add_disks.' + disk_idx, 1798 payload.add_disks[disk_idx]); 1799 } 1800 } 1801 } 1802 1803 if (BRAND_OPTIONS[brand].hasOwnProperty('required_properties')) { 1804 required = BRAND_OPTIONS[brand].required_properties; 1805 for (prop in required) { 1806 if (required[prop].indexOf(action) !== -1 1807 && !payload.hasOwnProperty(prop)) { 1808 1809 errors.missing_properties.push(prop); 1810 } 1811 } 1812 } 1813 1814 // make sure any images in the payload are also valid 1815 // NOTE: if validateImages() finds errors, it adds to 'errors' here. 1816 validateImages(payload, errors, log, function () { 1817 1818 // we validate disks.*.refreservation here because image_size might 1819 // not be populated yet until we return from validateImages() 1820 ['disks', 'add_disks'].forEach(function (d) { 1821 var d_idx = 0; 1822 if (payload.hasOwnProperty(d)) { 1823 payload[d].forEach(function (disk) { 1824 if (disk.hasOwnProperty('refreservation')) { 1825 if (disk.refreservation < 0) { 1826 errors.bad_values.push(d + '.' + d_idx 1827 + '.refreservation'); 1828 } else if (disk.size 1829 && disk.refreservation > disk.size) { 1830 1831 errors.bad_values.push(d + '.' + d_idx 1832 + '.refreservation'); 1833 } else if (disk.image_size 1834 && disk.refreservation > disk.image_size) { 1835 1836 errors.bad_values.push(d + '.' + d_idx 1837 + '.refreservation'); 1838 } 1839 } 1840 d_idx++; 1841 }); 1842 } 1843 }); 1844 1845 if (errors.bad_properties.length > 0 || errors.bad_values.length > 0 1846 || errors.missing_properties.length > 0) { 1847 1848 callback(errors); 1849 return; 1850 } 1851 1852 callback(); 1853 }); 1854 }); 1855 }; 1856 1857 function separateCommas(str) 1858 { 1859 return str.split(','); 1860 } 1861 1862 function unmangleMem(str) 1863 { 1864 return (Number(str) / (1024 * 1024)); 1865 } 1866 1867 function unbase64(str) 1868 { 1869 return new Buffer(str, 'base64').toString('ascii'); 1870 } 1871 1872 function numberify(str) 1873 { 1874 return Number(str); 1875 } 1876 1877 function startElement(name, attrs, state, log) { 1878 var disk = {}; 1879 var key; 1880 var newobj; 1881 var nic = {}; 1882 var obj; 1883 var prop; 1884 var stack; 1885 var use; 1886 var where; 1887 1888 assert(log, 'no logger passed to startElement()'); 1889 1890 if (!state.hasOwnProperty('stack')) { 1891 state.stack = []; 1892 } 1893 obj = state.obj; 1894 stack = state.stack; 1895 1896 stack.push(name); 1897 where = stack.join('.'); 1898 1899 if (XML_PROPERTIES.hasOwnProperty(where)) { 1900 for (key in XML_PROPERTIES[where]) { 1901 use = XML_PROPERTIES[where][key]; 1902 if (attrs.hasOwnProperty(key)) { 1903 obj[use] = attrs[key]; 1904 } else if (attrs.hasOwnProperty('name') && attrs.name === key) { 1905 // attrs use the whacky {name, type, value} stuff. 1906 obj[use] = attrs['value']; 1907 } 1908 } 1909 } else if (where === 'zone.rctl') { 1910 stack.push(attrs.name); 1911 } else if (where === 'zone.network') { 1912 // new network device 1913 for (prop in attrs) { 1914 if (XML_PROPERTIES.nic.hasOwnProperty(prop)) { 1915 use = XML_PROPERTIES.nic[prop]; 1916 if (prop === 'mac-addr') { 1917 // XXX SmartOS inherited the ridiculous MAC formatting from 1918 // Solaris where leading zeros are removed. We should 1919 // Fix that in the OS tools. 1920 nic[use] = fixMac(attrs[prop]); 1921 } else { 1922 nic[use] = attrs[prop]; 1923 } 1924 } else { 1925 log.debug('unknown net prop: ' + prop); 1926 } 1927 } 1928 if (!obj.hasOwnProperty('networks')) { 1929 obj.networks = {}; 1930 } 1931 obj.networks[nic.mac] = nic; 1932 stack.push(nic.mac); 1933 } else if (where.match(/zone\.network\...:..:..:..:..:..\.net-attr/)) { 1934 if (XML_PROPERTIES.nic.hasOwnProperty(attrs.name)) { 1935 use = XML_PROPERTIES.nic[attrs.name]; 1936 obj.networks[stack[2]][use] = attrs.value; 1937 } else { 1938 log.debug('unknown net prop: ' + attrs.name); 1939 } 1940 } else if (where === 'zone.device') { 1941 // new disk device 1942 for (prop in attrs) { 1943 if (XML_PROPERTIES.disk.hasOwnProperty(prop)) { 1944 use = XML_PROPERTIES.disk[prop]; 1945 disk[use] = attrs[prop]; 1946 } else { 1947 log.debug('unknown disk prop: ' + prop); 1948 } 1949 } 1950 if (!obj.hasOwnProperty('devices')) { 1951 obj.devices = {}; 1952 } 1953 obj.devices[disk.path] = disk; 1954 stack.push(disk.path); 1955 } else if (where.match(/zone\.device\.\/.*\.net-attr/)) { 1956 if (XML_PROPERTIES.disk.hasOwnProperty(attrs.name)) { 1957 use = XML_PROPERTIES.disk[attrs.name]; 1958 obj.devices[stack[2]][use] = attrs.value; 1959 } else { 1960 log.debug('unknown disk prop: ' + attrs.name); 1961 } 1962 } else if (where === 'zone.dataset') { 1963 if (!obj.hasOwnProperty('datasets')) { 1964 obj.datasets = []; 1965 } 1966 if (attrs.hasOwnProperty('name')) { 1967 obj.datasets.push(attrs.name); 1968 } 1969 } else if (where === 'zone.filesystem') { 1970 if (!obj.hasOwnProperty('filesystems')) { 1971 obj.filesystems = []; 1972 } 1973 newobj = {}; 1974 for (prop in XML_PROPERTIES.filesystem) { 1975 if (attrs.hasOwnProperty(prop)) { 1976 newobj[XML_PROPERTIES.filesystem[prop]] = attrs[prop]; 1977 } 1978 } 1979 obj.filesystems.push(newobj); 1980 } else if (where === 'zone.filesystem.fsoption') { 1981 newobj = obj.filesystems.slice(-1)[0]; // the last element 1982 if (!newobj.hasOwnProperty('options')) { 1983 newobj.options = []; 1984 } 1985 newobj.options.push(attrs.name); 1986 } else { 1987 log.debug('unknown property: ' + where + ': ' 1988 + JSON.stringify(attrs)); 1989 } 1990 } 1991 1992 function endElement(name, state) { 1993 // trim stack back above this element 1994 var stack = state.stack; 1995 1996 while (stack.pop() !== name) { 1997 // do nothing, we just want to consume. 1998 continue; 1999 } 2000 } 2001 2002 function indexSort(obj, field, pattern) 2003 { 2004 obj.sort(function (a, b) { 2005 var avalue = 0; 2006 var bvalue = 0; 2007 var matches; 2008 2009 if (a.hasOwnProperty(field)) { 2010 matches = a[field].match(pattern); 2011 if (matches) { 2012 avalue = Number(matches[1]); 2013 } 2014 } 2015 if (b.hasOwnProperty(field)) { 2016 matches = b[field].match(pattern); 2017 if (matches) { 2018 bvalue = Number(matches[1]); 2019 } 2020 } 2021 2022 return avalue - bvalue; 2023 }); 2024 } 2025 2026 function applyTransforms(obj) 2027 { 2028 var p; 2029 var pp; 2030 var subobj; 2031 var transforms = XML_PROPERTY_TRANSFORMS; 2032 2033 for (p in transforms) { 2034 if (obj.hasOwnProperty(p)) { 2035 if (typeof (transforms[p]) === 'object') { 2036 // this is a 'complex' property like nic, and has different 2037 // transforms for the sub-objects 2038 for (pp in transforms[p]) { 2039 for (subobj in obj[p]) { 2040 if (obj[p][subobj].hasOwnProperty(pp)) { 2041 obj[p][subobj][pp] = 2042 transforms[p][pp](obj[p][subobj][pp]); 2043 } 2044 } 2045 } 2046 } else { // function 2047 obj[p] = transforms[p](obj[p]); 2048 } 2049 } 2050 } 2051 } 2052 2053 // This function parses the zone XML file at /etc/zones/<zonename>.xml and adds 2054 // the VM properties to a new object. 2055 function getVmobj(zonename, preload_data, options, callback) 2056 { 2057 var filename = '/etc/zones/' + zonename + '.xml'; 2058 var log; 2059 var parser = new expat.Parser('UTF-8'); 2060 2061 assert(options.log, 'no logger passed to getVmobj()'); 2062 log = options.log; 2063 2064 fs.readFile(filename, function (error, data) { 2065 var allowed; 2066 var disk; 2067 var dsinfo; 2068 var fields; 2069 var nic; 2070 var obj = {}; 2071 var state = {}; 2072 2073 if (error) { 2074 callback(error); 2075 return; 2076 } 2077 2078 state.obj = obj; 2079 parser.on('startElement', function (name, attrs) { 2080 return startElement(name, attrs, state, log); 2081 }); 2082 parser.on('endElement', function (name) { 2083 return endElement(name, state); 2084 }); 2085 2086 if (!parser.parse(data.toString())) { 2087 throw new Error('There are errors in your xml file: ' 2088 + parser.getError()); 2089 } 2090 2091 // now that we know which brand we are, find out what we're allowed. 2092 allowed = BRAND_OPTIONS[obj.brand].allowed_properties; 2093 2094 // replace obj.networks with array of nics. 2095 obj.nics = []; 2096 for (nic in obj.networks) { 2097 obj.nics.push(obj.networks[nic]); 2098 } 2099 delete obj.networks; 2100 2101 // replace obj.devices with array of disks. 2102 if (allowed.hasOwnProperty('disks')) { 2103 obj.disks = []; 2104 for (disk in obj.devices) { 2105 obj.disks.push(obj.devices[disk]); 2106 } 2107 } 2108 delete obj.devices; 2109 2110 if (!BRAND_OPTIONS.hasOwnProperty(obj.brand)) { 2111 throw new Error('unable to handle brand ' + obj.brand); 2112 } 2113 2114 if (BRAND_OPTIONS[obj.brand].features.use_vm_autoboot) { 2115 obj.autoboot = obj.vm_autoboot; 2116 delete obj.vm_autoboot; 2117 } 2118 2119 // apply the XML_PROPERTY_TRANSFORMs 2120 applyTransforms(obj); 2121 2122 // probe for some fields on disks if this brand of zone supports them. 2123 if (allowed.hasOwnProperty('disks') 2124 && (allowed.disks.indexOf('create') !== -1)) { 2125 2126 for (disk in obj.disks) { 2127 disk = obj.disks[disk]; 2128 2129 if (preload_data.hasOwnProperty('dsinfo')) { 2130 dsinfo = preload_data.dsinfo; 2131 if (dsinfo.hasOwnProperty('mountpoints') 2132 && dsinfo.mountpoints.hasOwnProperty(disk.path)) { 2133 2134 disk.zfs_filesystem = dsinfo.mountpoints[disk.path]; 2135 disk.zpool = disk.zfs_filesystem.split('/')[0]; 2136 } else { 2137 log.trace('no mountpoint data for ' + disk.path); 2138 } 2139 } 2140 } 2141 } 2142 2143 if (obj.hasOwnProperty('transition')) { 2144 fields = rtrim(obj.transition).split(':'); 2145 if (fields.length === 3) { 2146 delete obj.transition; 2147 obj.state = fields[0]; 2148 obj.transition_to = fields[1]; 2149 obj.transition_expire = fields[2]; 2150 } else { 2151 log.debug('getVmobj() ignoring bad value for ' 2152 + 'transition "' + obj.transition + '"'); 2153 } 2154 } 2155 2156 // sort the disks + nics by index 2157 if (obj.hasOwnProperty('disks')) { 2158 indexSort(obj.disks, 'path', /^.*-disk(\d+)$/); 2159 } 2160 if (obj.hasOwnProperty('nics')) { 2161 indexSort(obj.nics, 'interface', /^net(\d+)$/); 2162 } 2163 if (obj.hasOwnProperty('filesystems')) { 2164 indexSort(obj.filesystems, 'target', /^(.*)$/); 2165 } 2166 2167 callback(null, obj); 2168 }); 2169 } 2170 2171 function setQuota(dataset, quota, log, callback) 2172 { 2173 var newval; 2174 2175 assert(log, 'no logger passed to setQuota()'); 2176 2177 if (!dataset) { 2178 callback(new Error('Invalid dataset: "' + dataset + '"')); 2179 return; 2180 } 2181 2182 if (quota === 0 || quota === '0') { 2183 newval = 'none'; 2184 } else { 2185 newval = quota.toString() + 'g'; 2186 } 2187 2188 zfs(['set', 'quota=' + newval, dataset], log, function (err, fds) { 2189 if (err) { 2190 log.error('setQuota() cmd failed: ' + fds.stderr); 2191 callback(new Error(rtrim(fds.stderr))); 2192 } else { 2193 callback(); 2194 } 2195 }); 2196 } 2197 2198 function cleanDatasetObject(obj) 2199 { 2200 var number_fields = [ 2201 'avail', 2202 'available', 2203 'copies', 2204 'creation', 2205 'filesystem_limit', 2206 'quota', 2207 'recsize', 2208 'recordsize', 2209 'refer', 2210 'referenced', 2211 'refquota', 2212 'refreserv', 2213 'refreservation', 2214 'reserv', 2215 'reservation', 2216 'snapshot_limit', 2217 'usedbychildren', 2218 'usedbydataset', 2219 'usedbyrefreservation', 2220 'usedbysnapshots', 2221 'used', 2222 'userrefs', 2223 'utf8only', 2224 'version', 2225 'volblock', 2226 'volblocksize', 2227 'volsize', 2228 'written' 2229 ]; 2230 2231 // We should always have mountpoint, dataset and type because we force them 2232 // to be included in zfsList() 2233 assert(obj.hasOwnProperty('mountpoint'), 'cleanDatasetObject(' 2234 + JSON.stringify(obj) + '): missing mountpoint'); 2235 assert(obj.hasOwnProperty('name'), 'cleanDatasetObject(' 2236 + JSON.stringify(obj) + '): missing name'); 2237 assert(obj.hasOwnProperty('type'), 'cleanDatasetObject(' 2238 + JSON.stringify(obj) + '): missing type'); 2239 2240 // convert numeric fields to proper numbers 2241 number_fields.forEach(function (field) { 2242 if (obj.hasOwnProperty(field) && obj[field] !== '-') { 2243 obj[field] = Number(obj[field]); 2244 } 2245 }); 2246 2247 if (obj.type === 'volume') { 2248 obj.mountpoint = '/dev/zvol/rdsk/' + obj.name; 2249 } else if (obj.mountpoint === '-' || obj.mountpoint === 'legacy') { 2250 obj.mountpoint = '/' + obj.name; 2251 } 2252 } 2253 2254 function addDatasetResult(fields, types, results, line, log) 2255 { 2256 var dataset; 2257 var field; 2258 var lfields; 2259 var obj; 2260 var snapparts; 2261 var snapobj; 2262 2263 line = trim(line); 2264 2265 if (line.length === 0) { 2266 return; 2267 } 2268 2269 lfields = line.split(/\s+/); 2270 2271 if (lfields.length !== fields.length) { 2272 return; 2273 } 2274 2275 obj = {}; 2276 2277 for (field in fields) { 2278 obj[fields[field]] = lfields[field]; 2279 } 2280 2281 cleanDatasetObject(obj); 2282 2283 if (!results.hasOwnProperty('datasets')) { 2284 results.datasets = {}; 2285 } 2286 if (!results.hasOwnProperty('mountpoints')) { 2287 results.mountpoints = {}; 2288 } 2289 if (types.indexOf('snapshot') !== -1 && obj.type === 'snapshot') { 2290 if (!results.hasOwnProperty('snapshots')) { 2291 results.snapshots = {}; 2292 } 2293 2294 /* 2295 * For snapshots we store the snapname and optionally creation keyed by 2296 * dataset name So that we can include the list of snapshots for a 2297 * dataset on a VM. 2298 */ 2299 snapparts = obj.name.split('@'); 2300 assert.equal(snapparts.length, 2); 2301 dataset = snapparts[0]; 2302 snapobj = {snapname: snapparts[1], dataset: dataset}; 2303 if (!results.snapshots.hasOwnProperty(dataset)) { 2304 results.snapshots[dataset] = []; 2305 } 2306 if (obj.hasOwnProperty('creation')) { 2307 snapobj.created_at = obj.creation; 2308 } 2309 results.snapshots[dataset].push(snapobj); 2310 } 2311 2312 results.datasets[obj.name] = obj; 2313 2314 /* 2315 * snapshots don't have mountpoint that we care about and we don't count 2316 * 'none' as a mountpoint. If we otherwise have a mountpoint that looks like 2317 * a path, we add a pointer from that to the dataset name. 2318 */ 2319 if (obj.type !== 'snapshot' && obj.mountpoint[0] === '/') { 2320 /* 2321 * For zoned filesystems (delegated datasets) we don't use mountpoint as 2322 * this can be changed from within the zone and is therefore not 2323 * reliable. Also, when a delegated dataset is assigned but the zone's 2324 * not been booted, the delegated dataset will not have the 'zoned' 2325 * property. So we also check if the name ends in /data. 2326 */ 2327 if (obj.hasOwnProperty('zoned') && obj.zoned === 'on') { 2328 // don't add zoned datasets to mountpoints 2329 /*jsl:pass*/ 2330 } else if (obj.name.split('/')[2] === 'data') { 2331 // name is /data, skip 2332 /*jsl:pass*/ 2333 } else { 2334 // here we have what looks like a normal non-zoned dataset that's 2335 // probably a zoneroot, add to mountpoints mapping. 2336 results.mountpoints[obj.mountpoint] = obj.name; 2337 } 2338 } 2339 } 2340 2341 /* 2342 * Arguments: 2343 * 2344 * 'fields' - should be an array of fields as listed in the zfs(1m) man page. 2345 * 'types' - should be one or more of: filesystem, snapshot, volume. 2346 * 'log' - should be a bunyan logger object. 2347 * 'callback' - will be called with (err, results) 2348 * 2349 * On failure: callback's err will be an Error object, ignore results. 2350 * On success: callback's results is an object with one or more members of: 2351 * 2352 * results.datasets 2353 * 2354 * keyed by dataset name containing the values for the requested fields. 2355 * 2356 * Eg: results.datasets['zones/cores'] === { name: 'zones/cores', ... } 2357 * 2358 * results.mountpoints 2359 * 2360 * keyed by mountpoint with value being dataset name. 2361 * 2362 * Eg: results.mountpoints['/zones/cores'] === 'zones/cores' 2363 * 2364 * results.snapshots 2365 * 2366 * keyed by dataset with value being array of snapname and created_at. 2367 * 2368 * Eg: results.snapshots['/zones/cores'] === ['snap1', ...] 2369 * 2370 * For non-zoned filesystem datasets (these should be the zoneroot datasets), 2371 * you can use mountpoint which comes from zoneadm's "cheap" info and use that 2372 * to get to the dataset and from datasets[dataset] get the info. 2373 * 2374 * For volumes (KVM VM's disks) you can also use mountpoint as we'll set that 2375 * to the block device path and that's available from the devices section of 2376 * the zoneconfig. 2377 * 2378 * For zoned filesystems (delegated datasets) use the dataset name, as the 2379 * mountpoint can be changed from within the zone. 2380 * 2381 */ 2382 function zfsList(fields, types, log, callback) { 2383 var args; 2384 var buffer = ''; 2385 var lines; 2386 var cmd = '/usr/sbin/zfs'; 2387 var req_fields = ['mountpoint', 'name', 'type']; 2388 var results = {}; 2389 var zfs_child; 2390 2391 assert(Array.isArray(types)); 2392 assert(Array.isArray(fields)); 2393 assert(log, 'no logger passed to zfsList()'); 2394 2395 // add any missing required fields 2396 req_fields.forEach(function (field) { 2397 if (fields.indexOf(field) === -1) { 2398 fields.push(field); 2399 } 2400 }); 2401 2402 args = ['list', '-H', '-p', '-t', types.join(','), '-o', fields.join(',')]; 2403 2404 log.debug(cmd + ' ' + args.join(' ')); 2405 2406 zfs_child = spawn(cmd, args, {'customFds': [-1, -1, -1]}); 2407 log.debug('zfs running with pid ' + zfs_child.pid); 2408 2409 zfs_child.stdout.on('data', function (data) { 2410 var line; 2411 2412 buffer += data.toString(); 2413 lines = buffer.split('\n'); 2414 while (lines.length > 1) { 2415 line = lines.shift(); 2416 2417 // Add this line to results 2418 addDatasetResult(fields, types, results, line, log); 2419 } 2420 buffer = lines.pop(); 2421 }); 2422 2423 // doesn't take input. 2424 zfs_child.stdin.end(); 2425 2426 zfs_child.on('exit', function (code) { 2427 log.debug('zfs process ' + zfs_child.pid + ' exited with code: ' 2428 + code); 2429 if (code === 0) { 2430 callback(null, results); 2431 } else { 2432 callback(new Error('zfs exited prematurely with code: ' + code)); 2433 } 2434 }); 2435 } 2436 2437 /* 2438 * This queue is used to handle zfs list requests. We do this because of OS-1834 2439 * in order to only run one 'zfs list' at a time. If we need to get data from 2440 * 'zfs list', the parameters we want to list are pushed onto this queue. If a 2441 * list is already running with the same parameters, we'll return the output 2442 * from that one when it completes to all the consumers. If there's not one 2443 * running, or the parameters are different, this set of parameters will be 2444 * pushed onto the tail of the queue. The queue is processed serially so long 2445 * as there are active requests. 2446 */ 2447 zfs_list_queue = async.queue(function (task, callback) { 2448 2449 var fields = task.fields; 2450 var log = task.log; 2451 var started = Date.now(0); 2452 var types = task.types; 2453 2454 zfsList(fields, types, log, function (err, data) { 2455 var emitter = zfs_list_in_progress[task]; 2456 2457 delete zfs_list_in_progress[task]; 2458 emitter.emit('result', err, data); 2459 emitter.removeAllListeners('result'); 2460 2461 log.debug('zfs list took ' + (Date.now(0) - started) + ' ms'); 2462 callback(); 2463 }); 2464 2465 }, 1); 2466 2467 zfs_list_queue.drain = function () { 2468 // We use the global log here because this queue is not tied to one action. 2469 VM.log.trace('zfs_list_queue is empty'); 2470 }; 2471 2472 function getZfsList(fields, types, log, callback) { 2473 var sorted_fields; 2474 var sorted_types; 2475 var task; 2476 2477 sorted_fields = fields.slice().sort(); 2478 sorted_types = types.slice().sort(); 2479 2480 task = {types: sorted_types, fields: sorted_fields, log: log}; 2481 2482 try { 2483 zfs_list_in_progress[task].on('result', callback); 2484 } catch (e) { 2485 if ((e instanceof TypeError) 2486 && (!zfs_list_in_progress.hasOwnProperty(task) 2487 || !zfs_list_in_progress[task].hasOwnProperty('on'))) { 2488 2489 zfs_list_in_progress[task] = new EventEmitter(); 2490 zfs_list_in_progress[task].on('result', callback); 2491 zfs_list_in_progress[task].setMaxListeners(0); 2492 zfs_list_queue.push(task); 2493 2494 // callback() will get called when 'result' is emitted. 2495 } else { 2496 callback(e); 2497 } 2498 } 2499 } 2500 2501 function loadDatasetInfo(fields, log, callback) 2502 { 2503 var zfs_fields = []; 2504 var zfs_types = []; 2505 2506 assert(log, 'no logger passed to loadDataset()'); 2507 2508 function addField(name) { 2509 if (zfs_fields.indexOf(name) === -1) { 2510 zfs_fields.push(name); 2511 } 2512 } 2513 2514 function addType(name) { 2515 if (zfs_types.indexOf(name) === -1) { 2516 zfs_types.push(name); 2517 } 2518 } 2519 2520 if (!fields || fields.length < 1) { 2521 // Default to grabbing everything we might possibly need. 2522 zfs_fields = ['name', 'quota', 'volsize', 'mountpoint', 'type', 2523 'compression', 'recsize', 'volblocksize', 'zoned', 'creation', 2524 'refreservation']; 2525 zfs_types = ['filesystem', 'snapshot', 'volume']; 2526 } else { 2527 if (fields.indexOf('disks') !== -1) { 2528 addField('compression'); 2529 addField('volsize'); 2530 addField('volblocksize'); 2531 addField('refreservation'); 2532 addType('volume'); 2533 } 2534 if (fields.indexOf('snapshots') !== -1) { 2535 addField('creation'); 2536 addType('snapshot'); 2537 addType('filesystem'); 2538 addType('volume'); 2539 } 2540 if (fields.indexOf('create_timestamp') !== -1) { 2541 // We might fall back to creation on the dataset for 2542 // create_timestamp if we have no create-timestamp attr. 2543 addField('creation'); 2544 addType('filesystem'); 2545 } 2546 if ((fields.indexOf('zfs_root_compression') !== -1) 2547 || (fields.indexOf('zfs_data_compression') !== -1)) { 2548 2549 addField('compression'); 2550 addType('filesystem'); 2551 } 2552 if ((fields.indexOf('zfs_root_recsize') !== -1) 2553 || (fields.indexOf('zfs_data_recsize') !== -1)) { 2554 2555 addField('recsize'); 2556 addType('filesystem'); 2557 } 2558 if (fields.indexOf('quota') !== -1) { 2559 addField('quota'); 2560 addType('filesystem'); 2561 } 2562 // both zpool and zfs_filesystem come from 'name' 2563 if (fields.indexOf('zpool') !== -1 2564 || fields.indexOf('zfs_filesystem') !== -1) { 2565 2566 addField('name'); 2567 addType('filesystem'); 2568 } 2569 if (zfs_fields.length > 0) { 2570 // we have some fields so we need to zfs, make sure we have name, 2571 // mountpoint and type which we always need if we get anything. 2572 addField('name'); 2573 addField('mountpoint'); 2574 addField('type'); 2575 2576 if (zfs_types.indexOf('filesystem') !== -1) { 2577 // to differentiate between delegated and root filesystems 2578 addField('zoned'); 2579 } 2580 } else { 2581 log.debug('no need to call zfs'); 2582 callback(null, { 2583 datasets: {}, 2584 mountpoints: {}, 2585 snapshots: {} 2586 }); 2587 return; 2588 } 2589 } 2590 2591 /* 2592 * NOTE: 2593 * in the future, the plan is to have the list of types and fields 2594 * be dynamic based what's actually needed to handle the request. 2595 */ 2596 2597 getZfsList(zfs_fields, zfs_types, log, callback); 2598 } 2599 2600 function loadJsonConfig(vmobj, cfg, log, callback) 2601 { 2602 var filename; 2603 2604 assert(log, 'no logger passed to loadJsonConfig()'); 2605 2606 if (vmobj.zonepath) { 2607 filename = vmobj.zonepath + '/config/' + cfg + '.json'; 2608 log.trace('loadJsonConfig() loading ' + filename); 2609 2610 fs.readFile(filename, function (error, data) { 2611 var json = {}; 2612 2613 if (error) { 2614 if (error.code === 'ENOENT') { 2615 log.debug('Skipping nonexistent file ' + filename); 2616 } else { 2617 log.error(error, 2618 'loadJsonConfig() failed to load ' + filename); 2619 callback(error, {}); 2620 return; 2621 } 2622 } else { 2623 try { 2624 json = JSON.parse(data.toString()); 2625 } catch (e) { 2626 json = {}; 2627 } 2628 } 2629 2630 callback(null, json); 2631 }); 2632 } else { 2633 callback(null, {}); 2634 } 2635 } 2636 2637 /* 2638 * This preloads some data for us that comes from commands which output for 2639 * *all* VMs. This allows us to just run these (expensive) commands once 2640 * instead of having to run them for each VM. 2641 * 2642 */ 2643 function preloadZoneData(uuid, options, callback) 2644 { 2645 var data = {}; 2646 var log; 2647 2648 assert(options.log, 'no logger passed to preloadZoneData()'); 2649 log = options.log; 2650 2651 // NOTE: uuid can be null, in which case we get data for all VMs. 2652 2653 async.series([ 2654 function (cb) { 2655 // We always do this (calls `zoneadm list -vc`) since we always 2656 // need to know which zones exist. 2657 getZoneRecords(uuid, log, function (err, records) { 2658 if (!err) { 2659 data.records = records; 2660 } 2661 cb(err); 2662 }); 2663 }, function (cb) { 2664 var fields; 2665 2666 if (options.hasOwnProperty('fields')) { 2667 fields = options.fields; 2668 } else { 2669 fields = []; 2670 } 2671 2672 loadDatasetInfo(fields, log, function (err, dsinfo) { 2673 if (!err) { 2674 data.dsinfo = dsinfo; 2675 } 2676 cb(err); 2677 }); 2678 }, function (cb) { 2679 if (options.hasOwnProperty('fields') 2680 && (options.fields.indexOf('server_uuid') === -1 2681 && options.fields.indexOf('datacenter_name') === -1 2682 && options.fields.indexOf('headnode_id') === -1)) { 2683 2684 // we don't need any fields that come from sysinfo. 2685 log.debug('no need to call sysinfo, no sysinfo fields in list'); 2686 data.sysinfo = {}; 2687 cb(); 2688 return; 2689 } 2690 2691 VM.getSysinfo([], {log: log}, function (err, sysinfo) { 2692 if (!err) { 2693 data.sysinfo = sysinfo; 2694 } 2695 cb(err); 2696 }); 2697 }, function (cb) { 2698 var u; 2699 var uuids = []; 2700 2701 if (options.hasOwnProperty('fields') 2702 && options.fields.indexOf('pid') === -1) { 2703 2704 log.debug('no need to check PID files, PID not in field list'); 2705 cb(); 2706 return; 2707 } 2708 2709 // get the PID values from running KVM VMs 2710 2711 for (u in data.records) { 2712 uuids.push(u); 2713 } 2714 async.forEachSeries(uuids, function (z_uuid, zcb) { 2715 var filename; 2716 var z = data.records[z_uuid]; 2717 2718 // NOTE: z.state here is equivalent to zone_state not state. 2719 if (z && BRAND_OPTIONS[z.brand].hasOwnProperty('features') 2720 && BRAND_OPTIONS[z.brand].features.pid_file 2721 && z.state === 'running') { 2722 2723 // ensure pid_file is safe 2724 try { 2725 assertSafeZonePath(path.join(z.zonepath, '/root'), 2726 BRAND_OPTIONS[z.brand].features.pid_file, 2727 {type: 'file', enoent_ok: true}); 2728 } catch (e) { 2729 // We log an error here, but not being able to get 2730 // the PID for one broken machine should not impact the 2731 // ability to get a list of all machines, so we just 2732 // skip adding a PID and log an error here. 2733 log.error(e, 'Unsafe path for ' + z.uuid + ' cannot ' 2734 + 'check for PID file: ' + e.message); 2735 zcb(); 2736 return; 2737 } 2738 2739 filename = path.join(z.zonepath, 'root', 2740 BRAND_OPTIONS[z.brand].features.pid_file); 2741 log.debug('checking for ' + filename); 2742 2743 fs.readFile(filename, 2744 function (error, filedata) { 2745 2746 var pid; 2747 2748 if (!error) { 2749 pid = Number(trim(filedata.toString())); 2750 if (pid > 0) { 2751 z.pid = pid; 2752 log.debug('found PID ' + pid + ' for ' 2753 + z.uuid); 2754 } 2755 } 2756 if (error && error.code === 'ENOENT') { 2757 // don't return error in this case because it just 2758 // didn't exist 2759 log.debug('no PID file for ' + z.uuid); 2760 zcb(); 2761 } else { 2762 zcb(error); 2763 } 2764 }); 2765 } else { 2766 zcb(); 2767 } 2768 }, function (err) { 2769 cb(err); 2770 }); 2771 } 2772 ], function (err, res) { 2773 log.trace('leaving preloadZoneData()'); 2774 callback(err, data); 2775 }); 2776 } 2777 2778 function getZoneRecords(uuid, log, callback) 2779 { 2780 var args = []; 2781 var buffer = ''; 2782 var cmd = '/usr/sbin/zoneadm'; 2783 var line_count = 0; 2784 var lines; 2785 var results = {}; 2786 var zadm; 2787 var zadm_stderr = ''; 2788 2789 assert(log, 'no logger passed to getZoneRecords()'); 2790 2791 if (uuid) { 2792 // this gives us zone info if uuid is *either* a zonename or uuid 2793 if (isUUID(uuid)) { 2794 args.push('-z'); 2795 args.push(uuid); 2796 args.push('-u'); 2797 args.push(uuid); 2798 } else { 2799 args.push('-z'); 2800 args.push(uuid); 2801 } 2802 } 2803 args.push('list'); 2804 args.push('-p'); 2805 if (!uuid) { 2806 args.push('-c'); 2807 } 2808 2809 log.debug(cmd + ' ' + args.join(' ')); 2810 2811 zadm = spawn(cmd, args, {'customFds': [-1, -1, -1]}); 2812 log.debug('zoneadm running with PID ' + zadm.pid); 2813 2814 zadm.stderr.on('data', function (data) { 2815 zadm_stderr += data.toString(); 2816 }); 2817 2818 zadm.stdout.on('data', function (data) { 2819 var fields; 2820 var line; 2821 var obj; 2822 2823 buffer += data.toString(); 2824 lines = buffer.split('\n'); 2825 while (lines.length > 1) { 2826 line = lines.shift(); 2827 line_count++; 2828 fields = rtrim(line).split(':'); 2829 if (fields.length === 8 && fields[1] !== 'global') { 2830 obj = { 2831 'zoneid': Number(fields[0]), 2832 'zonename': fields[1], 2833 'state': fields[2], 2834 'zonepath': fields[3], 2835 'uuid': fields[4], 2836 'brand': fields[5], 2837 'ip_type': fields[6] 2838 }; 2839 log.trace('loaded: ' + JSON.stringify(obj)); 2840 // XXX zones in some states have no uuid. We should either fix 2841 // that or use zonename for those. 2842 results[obj.uuid] = obj; 2843 } else if (line.replace(/ /g, '').length > 0) { 2844 log.debug('getZoneRecords(' + uuid + ') ignoring: ' + line); 2845 } 2846 } 2847 buffer = lines.pop(); 2848 }); 2849 2850 // doesn't take input. 2851 zadm.stdin.end(); 2852 2853 zadm.on('close', function (code) { 2854 var errmsg; 2855 var new_err; 2856 2857 log.debug('zoneadm process ' + zadm.pid + ' exited with code: ' 2858 + code + ' (' + line_count + ' lines to stdout)'); 2859 if (code === 0) { 2860 callback(null, results); 2861 } else { 2862 errmsg = rtrim(zadm_stderr); 2863 new_err = new Error(errmsg); 2864 if (errmsg.match(/No such zone configured$/)) { 2865 // not existing isn't always a problem (eg. existence check) 2866 new_err.code = 'ENOENT'; 2867 } else { 2868 log.error({err: new_err, stderr: zadm_stderr}, 2869 'getZoneRecords() zoneadm "' + args.join(',') + '" failed'); 2870 } 2871 callback(new_err); 2872 return; 2873 } 2874 }); 2875 } 2876 2877 exports.flatten = function (vmobj, key) 2878 { 2879 var index; 2880 var tokens = key.split('.'); 2881 2882 // NOTE: VM.flatten() currently doesn't produce any logs 2883 2884 if (tokens.length === 3 2885 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(tokens[0]) !== -1) { 2886 2887 if (!vmobj.hasOwnProperty(tokens[0])) { 2888 return undefined; 2889 } 2890 if (!vmobj[tokens[0]].hasOwnProperty(tokens[1])) { 2891 return undefined; 2892 } 2893 return vmobj[tokens[0]][tokens[1]][tokens[2]]; 2894 } 2895 2896 if (tokens.length === 2 2897 && VM.FLATTENABLE_HASH_KEYS.indexOf(tokens[0]) !== -1) { 2898 2899 if (!vmobj.hasOwnProperty(tokens[0])) { 2900 return undefined; 2901 } 2902 return vmobj[tokens[0]][tokens[1]]; 2903 } 2904 2905 if (tokens.length === 2 2906 && VM.FLATTENABLE_ARRAYS.indexOf(tokens[0]) !== -1) { 2907 2908 index = Number(tokens[1]); 2909 2910 if (!vmobj.hasOwnProperty(tokens[0])) { 2911 return undefined; 2912 } 2913 2914 if (index === NaN || index < 0 2915 || !vmobj[tokens[0]].hasOwnProperty(index)) { 2916 2917 return undefined; 2918 } 2919 return vmobj[tokens[0]][index]; 2920 } 2921 2922 return vmobj[key]; 2923 }; 2924 2925 function getLastModified(vmobj, log) 2926 { 2927 var files = []; 2928 var file; 2929 var stat; 2930 var timestamp = 0; 2931 2932 assert(log, 'no logger passed to getLastModified()'); 2933 2934 if (vmobj.zonepath) { 2935 files.push(path.join(vmobj.zonepath, '/config/metadata.json')); 2936 files.push(path.join(vmobj.zonepath, '/config/routes.json')); 2937 files.push(path.join(vmobj.zonepath, '/config/tags.json')); 2938 } else { 2939 log.debug('getLastModified() no zonepath!'); 2940 } 2941 2942 if (vmobj.hasOwnProperty('zonename')) { 2943 files.push('/etc/zones/' + vmobj.zonename + '.xml'); 2944 } else { 2945 log.debug('getLastModified() no zonename!'); 2946 } 2947 2948 for (file in files) { 2949 file = files[file]; 2950 try { 2951 stat = fs.statSync(file); 2952 if (stat.isFile()) { 2953 if ((timestamp === 0) || (Date.parse(stat.mtime) > timestamp)) { 2954 timestamp = Date.parse(stat.mtime); 2955 } 2956 } 2957 } catch (e) { 2958 if (e.code !== 'ENOENT') { 2959 log.error(e, 'Unable to get timestamp for "' + file + '":' 2960 + e.message); 2961 } 2962 } 2963 } 2964 2965 return ((new Date(timestamp)).toISOString()); 2966 } 2967 2968 function loadVM(uuid, data, options, callback) 2969 { 2970 var e; 2971 var info; 2972 var log; 2973 2974 assert(options.log, 'no logger passed to loadVM()'); 2975 log = options.log; 2976 2977 // XXX need to always have data when we get here 2978 info = data.records[uuid]; 2979 2980 if (!info) { 2981 e = new Error('VM.load() empty info when getting record ' 2982 + 'for vm ' + uuid); 2983 log.error(e); 2984 callback(e); 2985 return; 2986 } 2987 2988 getVmobj(info.zonename, data, options, function (err, vmobj) { 2989 if (err) { 2990 callback(err); 2991 return; 2992 } 2993 2994 function wantField(field) { 2995 if (options.hasOwnProperty('fields') 2996 && options.fields.indexOf(field) === -1) { 2997 2998 return false; 2999 } 3000 3001 return true; 3002 } 3003 3004 // We got some bits from `zoneadm list` as <info> here, and since we 3005 // already got that data, adding it to the object here is cheap. We also 3006 // need some of these properties to be able to get others later, so we 3007 // add them all now. If they're unwanted they'll be removed from the 3008 // final object. 3009 vmobj.uuid = info.uuid; 3010 vmobj.zone_state = info.state; 3011 3012 // In the case of 'configured' zones, we might only have zonename 3013 // because uuid isn't set yet. Because of that case, we set uuid 3014 // to zonename if it is in UUID form. 3015 if ((!vmobj.uuid || vmobj.uuid.length === 0) 3016 && isUUID(vmobj.zonename)) { 3017 3018 vmobj.uuid = vmobj.zonename; 3019 } 3020 3021 // These ones we never need elsewhere, so don't bother adding if we 3022 // don't need to. 3023 if (wantField('zoneid') && info.zoneid !== '-') { 3024 vmobj.zoneid = info.zoneid; 3025 } 3026 3027 if (wantField('pid') && info.pid) { 3028 vmobj.pid = info.pid; 3029 } 3030 3031 // find when we last modified this VM 3032 if (wantField('last_modified')) { 3033 vmobj.last_modified = getLastModified(vmobj, log); 3034 } 3035 3036 // If we want resolvers, (eg. OS-2194) we always add the array here 3037 // so you can tell that the resolvers are explicitly not set. 3038 if (wantField('resolvers') && !vmobj.hasOwnProperty('resolvers')) { 3039 vmobj.resolvers = []; 3040 } 3041 3042 // sysinfo has server_uuid and potentially some DC info 3043 if (data.hasOwnProperty('sysinfo')) { 3044 if (wantField('server_uuid') 3045 && data.sysinfo.hasOwnProperty('UUID')) { 3046 3047 vmobj.server_uuid = data.sysinfo.UUID; 3048 } 3049 if (wantField('datacenter_name') 3050 && data.sysinfo.hasOwnProperty('Datacenter Name')) { 3051 3052 vmobj.datacenter_name = data.sysinfo['Datacenter Name']; 3053 } 3054 if (wantField('headnode_id') 3055 && data.sysinfo.hasOwnProperty('Headnode ID')) { 3056 3057 vmobj.headnode_id = data.sysinfo['Headnode ID']; 3058 } 3059 } 3060 3061 // state could already be set here if it was overriden by a transition 3062 // that's in progress. So we only change if that's not the case. 3063 if (wantField('state')) { 3064 if (!vmobj.hasOwnProperty('state')) { 3065 if (info.state === 'installed') { 3066 vmobj.state = 'stopped'; 3067 } else { 3068 vmobj.state = info.state; 3069 } 3070 } 3071 3072 // If the zone has the 'failed' property it doesn't matter what 3073 // other state it might be in, we list its state as 'failed'. 3074 if (vmobj.failed) { 3075 vmobj.state = 'failed'; 3076 } 3077 } 3078 3079 async.series([ 3080 function (cb) { 3081 if (!wantField('customer_metadata') 3082 && !wantField('internal_metadata')) { 3083 3084 cb(); 3085 return; 3086 } 3087 3088 loadJsonConfig(vmobj, 'metadata', log, 3089 function (error, metadata) { 3090 if (error) { 3091 // when zone_state is 'incomplete' we could be 3092 // deleting it in which case metadata may already 3093 // be gone, ignore failure to load mdata when 3094 // 'incomplete' because of this. 3095 if (vmobj.zone_state === 'incomplete') { 3096 log.debug(error, 'zone is in state incomplete ' 3097 + 'ignoring error: ' + error.message); 3098 } else { 3099 cb(error); 3100 return; 3101 } 3102 } 3103 3104 if (wantField('customer_metadata')) { 3105 if (metadata.hasOwnProperty('customer_metadata')) { 3106 vmobj.customer_metadata 3107 = metadata.customer_metadata; 3108 } else { 3109 vmobj.customer_metadata = {}; 3110 } 3111 } 3112 3113 if (wantField('internal_metadata')) { 3114 if (metadata.hasOwnProperty('internal_metadata')) { 3115 vmobj.internal_metadata 3116 = metadata.internal_metadata; 3117 } else { 3118 vmobj.internal_metadata = {}; 3119 } 3120 } 3121 3122 cb(); 3123 }); 3124 }, function (cb) { 3125 if (!wantField('tags')) { 3126 cb(); 3127 return; 3128 } 3129 3130 loadJsonConfig(vmobj, 'tags', log, function (error, tags) { 3131 if (error) { 3132 // when zone_state is 'incomplete' we could be deleting 3133 // it in which case metadata may already be gone, ignore 3134 // failure to load mdata when 'incomplete' because of 3135 // this. 3136 if (vmobj.zone_state === 'incomplete') { 3137 log.debug(error, 'zone is in state incomplete ' 3138 + 'ignoring error: ' + error.message); 3139 } else { 3140 cb(error); 3141 return; 3142 } 3143 } 3144 vmobj.tags = tags; 3145 cb(); 3146 }); 3147 }, function (cb) { 3148 if (!wantField('routes')) { 3149 cb(); 3150 return; 3151 } 3152 3153 loadJsonConfig(vmobj, 'routes', log, function (error, routes) { 3154 if (error) { 3155 // same as tags above, if zone_state is 'incomplete' 3156 // we could be a file that's already gone 3157 if (vmobj.zone_state === 'incomplete') { 3158 log.debug(error, 'zone is in state incomplete ' 3159 + 'ignoring error: ' + error.message); 3160 } else { 3161 cb(error); 3162 return; 3163 } 3164 } 3165 vmobj.routes = routes; 3166 cb(); 3167 }); 3168 }, function (cb) { 3169 var dsinfo; 3170 var dsname; 3171 var dsobj; 3172 var d; 3173 var delegated; 3174 var disk; 3175 var ds; 3176 var filesys; 3177 var friendly_snap; 3178 var friendly_snapshots = []; 3179 var matches; 3180 var raw_snapshots = []; 3181 var snap; 3182 var snap_time; 3183 3184 // local alias, data.dsinfo should include all the info about 3185 // this VM's zoneroot that we care about here. 3186 dsinfo = data.dsinfo; 3187 3188 if (dsinfo.hasOwnProperty('mountpoints') 3189 && dsinfo.hasOwnProperty('datasets') 3190 && dsinfo.mountpoints.hasOwnProperty(vmobj.zonepath)) { 3191 3192 dsname = dsinfo.mountpoints[vmobj.zonepath]; 3193 dsobj = dsinfo.datasets[dsname]; 3194 3195 /* dsobj.quota is in bytes, we want GiB for vmobj.quota */ 3196 if (wantField('quota') && dsobj.hasOwnProperty('quota')) { 3197 vmobj.quota = (dsobj.quota / (1024 * 1024 * 1024)); 3198 log.trace('found quota "' + vmobj.quota + '" for ' 3199 + vmobj.uuid); 3200 } 3201 3202 if (wantField('create_timestamp') 3203 && !vmobj.hasOwnProperty('create_timestamp') 3204 && dsobj.hasOwnProperty('creation')) { 3205 3206 log.debug('VM has no create_timestamp, using creation ' 3207 + 'from ' + dsobj.name); 3208 vmobj.create_timestamp = 3209 (new Date(dsobj.creation * 1000)).toISOString(); 3210 } 3211 3212 if (wantField('zfs_root_compression') 3213 && dsobj.hasOwnProperty('compression') 3214 && (dsobj.compression !== 'off')) { 3215 3216 vmobj.zfs_root_compression = dsobj.compression; 3217 } 3218 3219 if (wantField('zfs_root_recsize') 3220 && dsobj.hasOwnProperty('recsize')) { 3221 3222 vmobj.zfs_root_recsize = dsobj.recsize; 3223 } 3224 3225 // Always add zfs_filesystem if we can because it's needed 3226 // to find other properties such as delegated_dataset. 3227 vmobj.zfs_filesystem = dsobj.name; 3228 3229 if (wantField('snapshots') 3230 && dsinfo.hasOwnProperty('snapshots') 3231 && dsinfo.snapshots 3232 .hasOwnProperty(vmobj.zfs_filesystem)) { 3233 3234 raw_snapshots = raw_snapshots.concat( 3235 dsinfo.snapshots[vmobj.zfs_filesystem]); 3236 } 3237 3238 log.trace('found dataset "' + vmobj.zfs_filesystem 3239 + '" for ' + vmobj.uuid); 3240 } else { 3241 log.trace('no dsinfo for ' + vmobj.uuid + ': ' 3242 + vmobj.zonepath); 3243 } 3244 3245 // delegated datasets are keyed on the dataset name instead of 3246 // mountpoint, since mountpoint can change in a zone. 3247 if (vmobj.hasOwnProperty('zfs_filesystem')) { 3248 delegated = vmobj.zfs_filesystem + '/data'; 3249 if (dsinfo.datasets.hasOwnProperty(delegated)) { 3250 dsobj = dsinfo.datasets[delegated]; 3251 3252 if (dsobj.hasOwnProperty('compression') 3253 && (dsobj.compression !== 'off')) { 3254 3255 vmobj.zfs_data_compression = dsobj.compression; 3256 } 3257 if (dsobj.hasOwnProperty('recsize')) { 3258 vmobj.zfs_data_recsize = dsobj.recsize; 3259 } 3260 3261 // If there are snapshots for this dataset, add them 3262 if (DISABLED) { 3263 // XXX currently only support snapshot on 3264 // zfs_filesystem 3265 if (dsinfo.hasOwnProperty('snapshots') 3266 && dsinfo.snapshots.hasOwnProperty(delegated)) { 3267 3268 raw_snapshots = raw_snapshots 3269 .concat(dsinfo.snapshots[delegated]); 3270 } 3271 } 3272 } else { 3273 log.trace('no dsinfo for delegated dataset: ' 3274 + delegated); 3275 } 3276 3277 vmobj.zpool = 3278 vmobj.zfs_filesystem.split('/')[0]; 3279 } 3280 3281 if (wantField('disks') && vmobj.hasOwnProperty('disks')) { 3282 for (d in vmobj.disks) { 3283 d = vmobj.disks[d]; 3284 if (d.hasOwnProperty('path') 3285 && dsinfo.mountpoints.hasOwnProperty(d.path)) { 3286 3287 dsname = dsinfo.mountpoints[d.path]; 3288 dsobj = dsinfo.datasets[dsname]; 3289 3290 if (dsobj.hasOwnProperty('volsize')) { 3291 3292 /* dsobj.volsize is in bytes, we want MiB */ 3293 d.size = (dsobj.volsize / (1024 * 1024)); 3294 log.debug('found size=' + d.size + ' for ' 3295 + JSON.stringify(d)); 3296 } 3297 if (dsobj.hasOwnProperty('compression')) { 3298 d.compression = dsobj.compression; 3299 } 3300 if (dsobj.hasOwnProperty('refreservation')) { 3301 /* dsobj.refreservation is in bytes, want MiB */ 3302 d.refreservation 3303 = (dsobj.refreservation / (1024 * 1024)); 3304 log.debug('found refreservation=' 3305 + d.refreservation + ' for ' 3306 + JSON.stringify(d)); 3307 } 3308 if (dsobj.hasOwnProperty('volblocksize')) { 3309 d.block_size = dsobj.volblocksize; 3310 } 3311 3312 // If there are snapshots for this dataset, add them 3313 // to the list. 3314 if (DISABLED) { 3315 // XXX currently only support snapshots on 3316 // zfs_filesystem 3317 if (dsinfo.hasOwnProperty('snapshots') 3318 && dsinfo.snapshots.hasOwnProperty( 3319 d.zfs_filesystem)) { 3320 3321 raw_snapshots = raw_snapshots.concat(dsinfo 3322 .snapshots[d.zfs_filesystem]); 3323 } 3324 } 3325 } else if (d.hasOwnProperty('path')) { 3326 d.missing = true; 3327 } else { 3328 log.warn('no dsinfo and no path for ' 3329 + JSON.stringify(d)); 3330 } 3331 } 3332 } 3333 3334 // snapshots here is the raw list of snapshots, now we need to 3335 // convert it to the "friendly" list of snapshots. 3336 if (wantField('snapshots')) { 3337 for (snap in raw_snapshots) { 3338 snap = raw_snapshots[snap]; 3339 3340 matches = snap.snapname.match(/^vmsnap-(.*)$/); 3341 if (matches && matches[1]) { 3342 friendly_snap = {name: matches[1]}; 3343 if (snap.hasOwnProperty('created_at')) { 3344 snap_time 3345 = new Date(snap.created_at * 1000); // in ms 3346 friendly_snap.created_at 3347 = snap_time.toISOString(); 3348 } 3349 friendly_snapshots.push(friendly_snap); 3350 } else { 3351 log.debug('ignoring unfriendly ' + snap.snapname); 3352 continue; 3353 } 3354 } 3355 // sort the snapshots with newest first. 3356 friendly_snapshots.sort(function (a, b) { 3357 if (a.created_at > b.created_at) { 3358 return -1; 3359 } 3360 if (a.created_at < b.created_at) { 3361 return 1; 3362 } 3363 return 0; // equal 3364 }); 3365 vmobj.snapshots = friendly_snapshots; 3366 } 3367 3368 if (vmobj.state === 'receiving') { 3369 vmobj.missing = { 'datasets': [], 'disks': [], 3370 'filesystems': [] }; 3371 if (!fs.existsSync(vmobj.zonepath)) { 3372 vmobj.missing.datasets.push(vmobj.zonepath.substr(1)); 3373 } 3374 for (ds in vmobj.datasets) { 3375 ds = vmobj.datasets[ds]; 3376 vmobj.missing.datasets.push(ds); 3377 } 3378 for (filesys in vmobj.filesystems) { 3379 filesys = vmobj.filesystems[filesys]; 3380 if (filesys.hasOwnProperty('source')) { 3381 vmobj.missing.filesystems.push(filesys.source); 3382 } 3383 } 3384 for (disk in vmobj.disks) { 3385 disk = vmobj.disks[disk]; 3386 if (disk.hasOwnProperty('missing')) { 3387 vmobj.missing.disks.push(disk.path); 3388 } 3389 } 3390 } 3391 3392 cb(); 3393 } 3394 ], function (error) { 3395 callback(error, vmobj); 3396 }); 3397 3398 }); 3399 } 3400 3401 exports.load = function (uuid, options, callback) 3402 { 3403 var log; 3404 var load_opts = {}; 3405 3406 // This is a wrapper so that other internal functions here (such as lookup) 3407 // can do smart things like check the quota for each VM with a separate call 3408 // to zfs get. 3409 3410 // options is optional 3411 if (arguments.length === 2) { 3412 callback = arguments[1]; 3413 options = {}; 3414 } 3415 3416 ensureLogging(false); 3417 if (options.hasOwnProperty('log')) { 3418 log = options.log; 3419 } else { 3420 log = VM.log.child({action: 'load', vm: uuid}); 3421 } 3422 3423 load_opts.log = log; 3424 if (options.hasOwnProperty('fields')) { 3425 load_opts.fields = options.fields; 3426 } 3427 3428 preloadZoneData(uuid, load_opts, function (error, data) { 3429 if (error) { 3430 if (options.missing_ok && error.code === 'ENOENT') { 3431 // we're expecting the zone to be gone in this case (eg. delete) 3432 log.debug('VM ' + uuid + ' does not exist (as expected)'); 3433 } else { 3434 log.error(error, 'VM.load() failed to get zone record' 3435 + ' for ' + uuid); 3436 } 3437 callback(error); 3438 } else { 3439 loadVM(uuid, data, load_opts, function (e, vmobj) { 3440 if (e) { 3441 callback(e); 3442 return; 3443 } 3444 3445 if (load_opts.hasOwnProperty('fields')) { 3446 // clean out unwanted fields 3447 Object.keys(vmobj).forEach(function (key) { 3448 if (options.fields.indexOf(key) === -1) { 3449 delete vmobj[key]; 3450 } 3451 }); 3452 } 3453 callback(null, vmobj); 3454 }); 3455 } 3456 }); 3457 }; 3458 3459 function fixMac(str) 3460 { 3461 var fixed = []; 3462 var octet; 3463 var octets = str.split(':'); 3464 3465 for (octet in octets) { 3466 if (octets.hasOwnProperty(octet)) { 3467 octet = parseInt(octets[octet], 16); 3468 if (octet === 'nan') { 3469 octet = 0; 3470 } 3471 fixed.push(sprintf('%02x', octet)); 3472 } 3473 } 3474 3475 return fixed.join(':'); 3476 } 3477 3478 // zonecfg requires removing leading 0's in MACs like 01:02:03:04:05:06 3479 // This function takes a MAC in normal form and puts it in the goofy form 3480 // zonecfg wants. 3481 function ruinMac(mac) 3482 { 3483 var part; 3484 var parts; 3485 var out = []; 3486 3487 parts = mac.split(':'); 3488 3489 for (part in parts) { 3490 part = ltrim(parts[part], '0'); 3491 if (part.length === 0) { 3492 part = '0'; 3493 } 3494 out.push(part); 3495 } 3496 3497 return (out.join(':')); 3498 } 3499 3500 function matcher(zone, search) 3501 { 3502 var fields; 3503 var found; 3504 var i; 3505 var key; 3506 var parameters_matched = 0; 3507 var regex; 3508 var target; 3509 3510 function find_match(k, targ) { 3511 var value = VM.flatten(zone, k); 3512 3513 if (!regex && k.match(/^nics\..*\.mac$/)) { 3514 // Fix for broken SmartOS MAC format 3515 targ = fixMac(targ); 3516 } 3517 3518 if (regex && (value !== undefined) && value.toString().match(targ)) { 3519 found = true; 3520 } else if ((value !== undefined) 3521 && value.toString() === targ.toString()) { 3522 found = true; 3523 } 3524 } 3525 3526 for (key in search) { 3527 found = false; 3528 regex = false; 3529 3530 target = search[key]; 3531 if (target[0] === '~') { 3532 regex = true; 3533 target = new RegExp(target.substr(1), 'i'); 3534 } 3535 3536 fields = key.split('.'); 3537 if (fields.length === 3 && fields[1] === '*' 3538 && zone.hasOwnProperty(fields[0]) 3539 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(fields[0]) !== -1) { 3540 3541 // Special case: for eg. nics.*.ip, we want to loop through all nics 3542 for (i = 0; i < zone[fields[0]].length; i++) { 3543 fields[1] = i; 3544 find_match(fields.join('.'), target); 3545 } 3546 } else { 3547 find_match(key, target); 3548 } 3549 3550 if (!found) { 3551 return false; 3552 } else { 3553 parameters_matched++; 3554 } 3555 } 3556 3557 if (parameters_matched > 0) { 3558 // we would have returned false from the loop had any parameters not 3559 // matched and we had at least one that did. 3560 return true; 3561 } 3562 3563 return false; 3564 } 3565 3566 exports.lookup = function (search, options, callback) 3567 { 3568 var log; 3569 var key; 3570 var matches; 3571 var need_fields = []; 3572 var preload_opts = {}; 3573 var quick_ok = true; 3574 var results = []; 3575 var transform; 3576 3577 // options is optional 3578 if (arguments.length === 2) { 3579 callback = arguments[1]; 3580 options = {}; 3581 } 3582 3583 ensureLogging(false); 3584 if (options.hasOwnProperty('log')) { 3585 log = options.log; 3586 } else { 3587 log = VM.log.child({action: 'lookup', search: search}); 3588 } 3589 3590 // XXX the 'transform' option is not intended to be public yet and should 3591 // only be used by tools willing to be rewritten if this is removed or 3592 // changed. 3593 if (options.hasOwnProperty('transform')) { 3594 transform = options.transform; 3595 } 3596 3597 // keep separate variable because we can have some fields we add below that 3598 // we need for searching, but shouldn't be in the output. 3599 if (options.hasOwnProperty('fields')) { 3600 need_fields = options.fields.slice(0); 3601 } 3602 3603 for (key in search) { 3604 // To be able to search on a field, that field needs to be added to 3605 // the objects, if user requested a set of fields missing the one 3606 // they're searching for, add it. 3607 matches = key.match(/^([^.]+)\./); 3608 if (matches) { 3609 if (need_fields.indexOf(matches[1]) == -1) { 3610 need_fields.push(matches[1]); 3611 } 3612 } else { 3613 if (need_fields.indexOf(key) == -1) { 3614 need_fields.push(key); 3615 } 3616 } 3617 } 3618 3619 // If all the keys we're searching for are in the QUICK_LOOKUP data, we 3620 // don't need the full zone records to locate the VMs we're interested in. 3621 for (key in need_fields) { 3622 if (QUICK_LOOKUP.indexOf(key) === -1) { 3623 quick_ok = false; 3624 } 3625 } 3626 3627 preload_opts.log = log; 3628 if (options.hasOwnProperty('fields')) { 3629 preload_opts.fields = need_fields; 3630 } 3631 3632 // This is used when you've specified fields to remove those that might 3633 // have been added as a group but are not wanted, or were added as 3634 // dependencies for looking up wanted fields, or for search. 3635 function filterFields(res) { 3636 res.forEach(function (result) { 3637 Object.keys(result).forEach(function (k) { 3638 if (options.fields.indexOf(k) === -1) { 3639 delete result[k]; 3640 } 3641 }); 3642 }); 3643 } 3644 3645 preloadZoneData(null, preload_opts, function (err, data) { 3646 var records = data.records; 3647 var uuids = []; 3648 3649 if (err) { 3650 callback(err); 3651 return; 3652 } 3653 3654 if (quick_ok) { 3655 var full_results = []; 3656 var load_opts = {}; 3657 var match; 3658 var regex; 3659 var source; 3660 var target; 3661 var u; 3662 var z; 3663 3664 if (err) { 3665 callback(err); 3666 return; 3667 } 3668 for (z in records) { 3669 z = records[z]; 3670 match = true; 3671 for (key in search) { 3672 regex = false; 3673 // force field type to string so that earlier transformed 3674 // number fields get back their match method and the 3675 // strict not-equal operator will work on number lookups 3676 source = '' + z[key]; 3677 target = search[key]; 3678 if (target[0] === '~') { 3679 target = new RegExp(target.substr(1), 'i'); 3680 regex = true; 3681 } 3682 if (regex && !source.match(target)) { 3683 match = false; 3684 } else if (!regex && (source !== search[key])) { 3685 match = false; 3686 } 3687 } 3688 if (match && z.uuid) { 3689 results.push(z.uuid); 3690 } 3691 } 3692 3693 load_opts.log = log; 3694 if (options.hasOwnProperty('fields') && need_fields.length > 0) { 3695 // we have a specific set of fields we want to grab 3696 load_opts.fields = need_fields; 3697 } else if (!options.full) { 3698 // we don't need all the data so what we already got is enough 3699 if (options.hasOwnProperty('fields')) { 3700 filterFields(results); 3701 } 3702 3703 callback(null, 3704 results.filter(function (res) { 3705 if (typeof (res) === 'object') { 3706 return (Object.keys(res).length > 0); 3707 } else { 3708 return (true); 3709 } 3710 }) 3711 ); 3712 return; 3713 } 3714 3715 function expander(uuid, cb) { 3716 loadVM(uuid, data, load_opts, function (e, obj) { 3717 if (e) { 3718 if (e.code === 'ENOENT') { 3719 // zone likely was deleted since lookup, ignore 3720 cb(); 3721 } else { 3722 cb(e); 3723 } 3724 } else { 3725 if (transform) { 3726 transform(obj); 3727 } 3728 full_results.push(obj); 3729 cb(); 3730 } 3731 }); 3732 } 3733 3734 async.forEachSeries(results, expander, function (e) { 3735 var res_list; 3736 3737 if (e) { 3738 log.error(e, 'VM.lookup failed to expand results: ' 3739 + e.message); 3740 callback(e); 3741 } else { 3742 res_list = full_results; 3743 if (options.hasOwnProperty('fields')) { 3744 filterFields(res_list); 3745 } 3746 callback(null, 3747 res_list.filter(function (res) { 3748 if (typeof (res) === 'object') { 3749 return (Object.keys(res).length > 0); 3750 } else { 3751 return (true); 3752 } 3753 }) 3754 ); 3755 } 3756 }); 3757 } else { 3758 // have to search the hard way (through all the data) 3759 for (u in records) { 3760 uuids.push(u); 3761 } 3762 // this is parallel! 3763 async.forEach(uuids, function (uuid, cb) { 3764 var vmobj = records[uuid]; 3765 var l_opts = {log: log}; 3766 3767 if (options.hasOwnProperty('fields') 3768 && need_fields.length > 0) { 3769 3770 // we have a specific set of fields we want to grab 3771 l_opts.fields = need_fields; 3772 } 3773 3774 loadVM(vmobj.uuid, data, l_opts, function (error, obj) { 3775 if (error) { 3776 if (error.code === 'ENOENT') { 3777 // zone likely was deleted since lookup, ignore 3778 cb(); 3779 } else { 3780 cb(error); 3781 } 3782 } else { 3783 if (transform) { 3784 transform(obj); 3785 } 3786 if (Object.keys(search).length === 0 3787 || matcher(obj, search)) { 3788 3789 results.push(obj); 3790 } 3791 cb(); 3792 } 3793 }); 3794 }, function (e) { 3795 var r; 3796 var short_results = []; 3797 3798 if (e) { 3799 callback(e); 3800 } else { 3801 if (options.full) { 3802 callback(null, results); 3803 } else if (options.fields && need_fields.length > 0) { 3804 if (options.hasOwnProperty('fields')) { 3805 filterFields(results); 3806 } 3807 callback(null, 3808 results.filter(function (res) { 3809 if (typeof (res) === 'object') { 3810 return (Object.keys(res).length > 0); 3811 } else { 3812 return (true); 3813 } 3814 }) 3815 ); 3816 } else { 3817 for (r in results) { 3818 short_results.push(results[r].uuid); 3819 } 3820 callback(null, short_results); 3821 } 3822 } 3823 }); 3824 } 3825 }); 3826 }; 3827 3828 // create a random new locally administered MAC address 3829 function generateMAC() 3830 { 3831 var data = [(Math.floor(Math.random() * 15) + 1).toString(16) + 2]; 3832 for (var i = 0; i < 5; i++) { 3833 var oct = (Math.floor(Math.random() * 255) + 1).toString(16); 3834 if (oct.length == 1) { 3835 oct = '0' + oct; 3836 } 3837 data.push(oct); 3838 } 3839 3840 return data.join(':'); 3841 } 3842 3843 // return the MAC address based on a VRRP Virtual Router ID 3844 function vrrpMAC(vrid) { 3845 return sprintf('00:00:5e:00:01:%02x', vrid); 3846 } 3847 3848 // Ensure we've got all the datasets necessary to create this VM 3849 // 3850 // IMPORTANT: 3851 // 3852 // On SmartOS, we assume a provisioner or some other external entity has already 3853 // loaded the dataset into the system. This function just confirms that the 3854 // dataset actually exists. 3855 // 3856 function checkDatasets(payload, log, callback) 3857 { 3858 var checkme = []; 3859 var d; 3860 var disk; 3861 3862 assert(log, 'no logger passed to checkDatasets()'); 3863 3864 log.debug('Checking for required datasets.'); 3865 3866 // build list of datasets we need to download (downloadme) 3867 for (disk in payload.add_disks) { 3868 if (payload.add_disks.hasOwnProperty(disk)) { 3869 d = payload.add_disks[disk]; 3870 if (d.hasOwnProperty('image_uuid')) { 3871 checkme.push(payload.zpool + '/' 3872 + d.image_uuid); 3873 } 3874 } 3875 } 3876 3877 function checker(dataset, cb) { 3878 zfs(['list', '-o', 'name', '-H', dataset], log, function (err, fds) { 3879 if (err) { 3880 log.error({'err': err, 'stdout': fds.stdout, 3881 'stderr': fds.stderr}, 'zfs list ' + dataset + ' ' 3882 + 'exited with' + ' code ' + err.code + ': ' + err.message); 3883 cb(new Error('unable to find dataset: ' + dataset)); 3884 } else { 3885 cb(); 3886 } 3887 }); 3888 } 3889 3890 // check that we have all the volumes 3891 async.forEachSeries(checkme, checker, function (err) { 3892 if (err) { 3893 log.error(err, 'checkDatasets() failed to find required ' 3894 + 'volumes'); 3895 callback(err); 3896 } else { 3897 // progress(100, 'we have all necessary datasets'); 3898 callback(); 3899 } 3900 }); 3901 } 3902 3903 function lookupConflicts(macs, ips, vrids, log, callback) { 3904 var conflict = false; 3905 var load_fields; 3906 3907 load_fields = ['brand', 'state', 'nics', 'uuid', 'zonename', 'zone_state']; 3908 3909 assert(log, 'no logger passed to lookupConflicts()'); 3910 3911 log.debug('checking for conflicts with ' 3912 + JSON.stringify(macs) + ', ' + JSON.stringify(ips) + ' and ' 3913 + JSON.stringify(vrids)); 3914 3915 if (macs.length === 0 && ips.length === 0 && vrids.length === 0) { 3916 log.debug('returning from conflict check (nothing to check)'); 3917 callback(null, conflict); 3918 return; 3919 } 3920 3921 preloadZoneData(null, {fields: load_fields, log: log}, 3922 function (err, data) { 3923 3924 var records = data.records; 3925 var uuid; 3926 var uuids = []; 3927 3928 if (err) { 3929 callback(err); 3930 return; 3931 } 3932 3933 for (uuid in records) { 3934 uuids.push(uuid); 3935 } 3936 3937 // this is parallel! 3938 async.forEach(uuids, function (z_uuid, cb) { 3939 loadVM(z_uuid, data, {fields: load_fields, log: log}, 3940 function (error, obj) { 3941 3942 var ip; 3943 var mac; 3944 var vrid; 3945 3946 if (error) { 3947 if (error.code === 'ENOENT') { 3948 // zone likely was deleted since lookup, ignore it 3949 cb(); 3950 } else { 3951 cb(error); 3952 } 3953 return; 3954 } 3955 3956 if (obj.state === 'failed' && obj.zone_state !== 'running') { 3957 // Ignore zones that are failed unless they're 'running' 3958 // which they shouldn't be because they get stopped on 3959 // failure. 3960 cb(); 3961 return; 3962 } 3963 3964 for (ip in ips) { 3965 if (ips[ip] !== 'dhcp' 3966 && matcher(obj, {'nics.*.ip': ips[ip]})) { 3967 3968 log.error('Found conflict: ' + obj.uuid 3969 + ' already has IP ' + ips[ip]); 3970 conflict = true; 3971 } 3972 } 3973 for (mac in macs) { 3974 if (matcher(obj, {'nics.*.mac': macs[mac]})) { 3975 log.error('Found conflict: ' + obj.uuid 3976 + ' already has MAC ' + macs[mac]); 3977 conflict = true; 3978 } 3979 } 3980 for (vrid in vrids) { 3981 if (matcher(obj, {'nics.*.vrrp_vrid': vrids[vrid]})) { 3982 log.error('Found conflict: ' + obj.uuid 3983 + ' already has VRID ' + vrids[vrid]); 3984 conflict = true; 3985 } 3986 } 3987 cb(); 3988 }); 3989 }, function (e) { 3990 if (e) { 3991 callback(e); 3992 } else { 3993 log.debug('returning from conflict check'); 3994 callback(null, conflict); 3995 } 3996 }); 3997 }); 3998 } 3999 4000 function lookupInvalidNicTags(nics, log, callback) { 4001 var etherstubs = []; 4002 var nic_tags = {}; 4003 4004 assert(log, 'no logger passed to lookupInvalidNicTags()'); 4005 4006 if (!nics || nics.length === 0) { 4007 callback(); 4008 return; 4009 } 4010 4011 async.parallel([ 4012 function (cb) { 4013 dladm.showEtherstub(null, log, function (err, stubs) { 4014 if (err) { 4015 cb(err); 4016 } else { 4017 etherstubs = stubs; 4018 cb(); 4019 } 4020 }); 4021 }, function (cb) { 4022 VM.getSysinfo([], {log: log}, function (err, sysinfo) { 4023 if (err) { 4024 cb(err); 4025 } else { 4026 var nic; 4027 var tag; 4028 for (nic in sysinfo['Network Interfaces']) { 4029 nic = sysinfo['Network Interfaces'][nic]; 4030 for (tag in nic['NIC Names']) { 4031 nic_tags[nic['NIC Names'][tag]] = 1; 4032 } 4033 } 4034 cb(); 4035 } 4036 }); 4037 } 4038 ], function (err, results) { 4039 if (err) { 4040 callback(err); 4041 return; 4042 } 4043 4044 var nic; 4045 for (nic in nics) { 4046 nic = nics[nic]; 4047 if (!nic.hasOwnProperty('nic_tag')) { 4048 continue; 4049 } 4050 if (!nic_tags.hasOwnProperty(nic.nic_tag) 4051 && (etherstubs.indexOf(nic.nic_tag) === -1)) { 4052 callback(new Error('Invalid nic tag "' + nic.nic_tag + '"')); 4053 return; 4054 } 4055 } 4056 4057 callback(); 4058 return; 4059 }); 4060 } 4061 4062 // create a new zvol for a VM 4063 function createVolume(volume, log, callback) 4064 { 4065 var refreserv; 4066 var size; 4067 var snapshot; 4068 4069 assert(log, 'no logger passed for createVolume()'); 4070 4071 log.debug('creating volume ' + JSON.stringify(volume)); 4072 4073 if (volume.hasOwnProperty('image_size')) { 4074 size = volume.image_size; 4075 } else if (volume.hasOwnProperty('size')) { 4076 size = volume.size; 4077 } else { 4078 callback(new Error('FATAL: createVolume(' + JSON.stringify(volume) 4079 + '): ' + 'has no size or image_size')); 4080 return; 4081 } 4082 4083 if (volume.hasOwnProperty('refreservation')) { 4084 refreserv = volume.refreservation; 4085 } else { 4086 log.debug('defaulting to refreservation = ' + size); 4087 refreserv = size; 4088 } 4089 4090 async.series([ 4091 function (cb) { 4092 if (volume.hasOwnProperty('image_uuid')) { 4093 snapshot = volume.zpool + '/' + volume.image_uuid + '@final'; 4094 zfs(['get', '-Ho', 'value', 'name', snapshot], log, 4095 function (err, fds) { 4096 4097 if (err) { 4098 if (fds.stderr.match('dataset does not exist')) { 4099 // no @final, so we'll make a new snapshot @<uuid> 4100 snapshot = volume.zpool + '/' + volume.image_uuid 4101 + '@' + volume.uuid; 4102 4103 zfs(['snapshot', snapshot], log, function (e) { 4104 cb(e); 4105 }); 4106 } else { 4107 cb(err); 4108 } 4109 } else { 4110 // @final is here! 4111 cb(); 4112 } 4113 }); 4114 } else { 4115 cb(); 4116 } 4117 }, function (cb) { 4118 var args; 4119 var target; 4120 4121 target = volume.zpool + '/' + volume.uuid; 4122 if (volume.hasOwnProperty('image_uuid')) { 4123 // This volume is from a template/dataset/image so we create 4124 // it as a clone of a the @final snapshot on the original. 4125 // we already set 'snapshot' to the correct location above. 4126 args = ['clone', '-F']; 4127 if (volume.hasOwnProperty('compression')) { 4128 args.push('-o', 'compression=' 4129 + volume.compression); 4130 } 4131 if (volume.hasOwnProperty('block_size')) { 4132 args.push('-o', 'volblocksize=' 4133 + volume.block_size); 4134 } 4135 args.push('-o', 'refreservation=' + refreserv + 'M'); 4136 args.push(snapshot, target); 4137 zfs(args, log, function (e) { 4138 if (e) { 4139 cb(e); 4140 } else { 4141 volume.path = '/dev/zvol/rdsk/' + target; 4142 cb(); 4143 } 4144 }); 4145 } else { 4146 // This volume is not from a template/dataset/image so we create 4147 // a blank new zvol for it. 4148 args = ['create']; 4149 if (volume.hasOwnProperty('compression')) { 4150 args.push('-o', 'compression=' 4151 + volume.compression); 4152 } 4153 if (volume.hasOwnProperty('block_size')) { 4154 args.push('-o', 'volblocksize=' 4155 + volume.block_size); 4156 } 4157 args.push('-o', 'refreservation=' + refreserv + 'M', '-V', 4158 size + 'M', target); 4159 zfs(args, log, function (err, fds) { 4160 if (err) { 4161 cb(err); 4162 } else { 4163 volume.path = '/dev/zvol/rdsk/' + target; 4164 cb(); 4165 } 4166 }); 4167 } 4168 } 4169 ], function (err, results) { 4170 callback(err); 4171 }); 4172 } 4173 4174 // Create all the volumes for a given VM property set 4175 function createVolumes(payload, log, callback) 4176 { 4177 var createme = []; 4178 var d; 4179 var disk; 4180 var disk_idx = 0; 4181 var used_disk_indexes = []; 4182 4183 assert(log, 'no logger passed to createVolumes()'); 4184 4185 log.debug('creating volumes: ' + JSON.stringify(payload.add_disks)); 4186 4187 if (payload.hasOwnProperty('used_disk_indexes')) { 4188 used_disk_indexes = payload.used_disk_indexes; 4189 } 4190 4191 for (disk in payload.add_disks) { 4192 if (payload.add_disks.hasOwnProperty(disk)) { 4193 d = payload.add_disks[disk]; 4194 4195 // we don't create CDROM devices or disk devices which have the 4196 // nocreate: true property. 4197 if (d.media !== 'cdrom' && !d.nocreate) { 4198 // skip to the next unused one. 4199 while (used_disk_indexes.indexOf(disk_idx) !== -1) { 4200 disk_idx++; 4201 } 4202 4203 d.index = disk_idx; 4204 d.uuid = payload.uuid + '-disk' + disk_idx; 4205 used_disk_indexes.push(Number(disk_idx)); 4206 if (!d.hasOwnProperty('zpool')) { 4207 d.zpool = payload.zpool; 4208 } 4209 createme.push(d); 4210 } 4211 } 4212 } 4213 4214 function loggedCreateVolume(volume, cb) { 4215 return createVolume(volume, log, cb); 4216 } 4217 4218 // create all the volumes we found that we need. 4219 async.forEachSeries(createme, loggedCreateVolume, function (err) { 4220 if (err) { 4221 callback(err); 4222 } else { 4223 callback(); 4224 } 4225 }); 4226 } 4227 4228 // writes a Zone's metadata JSON to /zones/<uuid>/config/metadata.json 4229 // and /zones/<uuid>/config/tags.json. 4230 function updateMetadata(vmobj, payload, log, callback) 4231 { 4232 var cmdata = {}; 4233 var imdata = {}; 4234 var key; 4235 var mdata = {}; 4236 var mdata_filename; 4237 var tags = {}; 4238 var tags_filename; 4239 var zonepath; 4240 4241 assert(log, 'no logger passed to updateMetadata()'); 4242 4243 if (vmobj.hasOwnProperty('zonepath')) { 4244 zonepath = vmobj.zonepath; 4245 } else if (vmobj.hasOwnProperty('zpool') 4246 && vmobj.hasOwnProperty('zonename')) { 4247 4248 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename; 4249 } else { 4250 callback(new Error('unable to find zonepath for ' 4251 + JSON.stringify(vmobj))); 4252 return; 4253 } 4254 4255 // paths are under zonepath but not zoneroot 4256 mdata_filename = zonepath + '/config/metadata.json'; 4257 tags_filename = zonepath + '/config/tags.json'; 4258 4259 // customer_metadata 4260 for (key in vmobj.customer_metadata) { 4261 if (vmobj.customer_metadata.hasOwnProperty(key)) { 4262 cmdata[key] = vmobj.customer_metadata[key]; 4263 if (payload.hasOwnProperty('remove_customer_metadata') 4264 && payload.remove_customer_metadata.indexOf(key) !== -1) { 4265 4266 // in the remove_* list, don't load it. 4267 delete cmdata[key]; 4268 } 4269 } 4270 } 4271 4272 for (key in payload.set_customer_metadata) { 4273 if (payload.set_customer_metadata.hasOwnProperty(key)) { 4274 cmdata[key] = payload.set_customer_metadata[key]; 4275 } 4276 } 4277 4278 // internal_metadata 4279 for (key in vmobj.internal_metadata) { 4280 if (vmobj.internal_metadata.hasOwnProperty(key)) { 4281 imdata[key] = vmobj.internal_metadata[key]; 4282 if (payload.hasOwnProperty('remove_internal_metadata') 4283 && payload.remove_internal_metadata.indexOf(key) !== -1) { 4284 4285 // in the remove_* list, don't load it. 4286 delete imdata[key]; 4287 } 4288 } 4289 } 4290 4291 for (key in payload.set_internal_metadata) { 4292 if (payload.set_internal_metadata.hasOwnProperty(key)) { 4293 imdata[key] = payload.set_internal_metadata[key]; 4294 } 4295 } 4296 4297 // same thing for tags 4298 for (key in vmobj.tags) { 4299 if (vmobj.tags.hasOwnProperty(key)) { 4300 tags[key] = vmobj.tags[key]; 4301 if (payload.hasOwnProperty('remove_tags') 4302 && payload.remove_tags.indexOf(key) !== -1) { 4303 4304 // in the remove_* list, don't load it. 4305 delete tags[key]; 4306 } 4307 } 4308 } 4309 4310 for (key in payload.set_tags) { 4311 if (payload.set_tags.hasOwnProperty(key)) { 4312 tags[key] = payload.set_tags[key]; 4313 } 4314 } 4315 4316 mdata = {'customer_metadata': cmdata, 'internal_metadata': imdata}; 4317 fs.writeFile(mdata_filename, JSON.stringify(mdata, null, 2), 4318 function (err) { 4319 if (err) { 4320 callback(err); 4321 } else { 4322 log.debug('wrote metadata to ' + mdata_filename); 4323 fs.writeFile(tags_filename, JSON.stringify(tags, null, 2), 4324 function (e) { 4325 if (e) { 4326 callback(e); 4327 } else { 4328 log.debug('wrote tags to' + tags_filename); 4329 callback(); 4330 } 4331 } 4332 ); 4333 } 4334 } 4335 ); 4336 } 4337 4338 function saveMetadata(payload, log, callback) 4339 { 4340 var protovm = {}; 4341 4342 assert(log, 'no logger passed to saveMetadata()'); 4343 4344 if (!payload.hasOwnProperty('zonepath') 4345 || !payload.hasOwnProperty('zpool') 4346 || !payload.hasOwnProperty('zonename')) { 4347 4348 callback(new Error('saveMetadata payload is missing zone ' 4349 + 'properties.')); 4350 return; 4351 } 4352 4353 protovm.zonepath = payload.zonepath; 4354 protovm.zpool = payload.zpool; 4355 protovm.zonename = payload.zonename; 4356 protovm.customer_metadata = {}; 4357 protovm.tags = {}; 4358 4359 if (payload.hasOwnProperty('tags')) { 4360 payload.set_tags = payload.tags; 4361 delete payload.tags; 4362 } 4363 if (payload.hasOwnProperty('customer_metadata')) { 4364 payload.set_customer_metadata = payload.customer_metadata; 4365 delete payload.customer_metadata; 4366 } 4367 if (payload.hasOwnProperty('internal_metadata')) { 4368 payload.set_internal_metadata = payload.internal_metadata; 4369 delete payload.internal_metadata; 4370 } 4371 4372 updateMetadata(protovm, payload, log, callback); 4373 } 4374 4375 // writes a zone's metadata JSON to /zones/<uuid>/config/routes.json 4376 function updateRoutes(vmobj, payload, log, callback) 4377 { 4378 var filename; 4379 var key; 4380 var routes = {}; 4381 var zonepath; 4382 4383 assert(log, 'no logger passed to updateRoutes()'); 4384 4385 if (vmobj.hasOwnProperty('zonepath')) { 4386 zonepath = vmobj.zonepath; 4387 } else if (vmobj.hasOwnProperty('zpool') 4388 && vmobj.hasOwnProperty('zonename')) { 4389 4390 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename; 4391 } else { 4392 callback(new Error('unable to find zonepath for ' 4393 + JSON.stringify(vmobj))); 4394 return; 4395 } 4396 4397 // paths are under zonepath but not zoneroot 4398 filename = zonepath + '/config/routes.json'; 4399 4400 for (key in vmobj.routes) { 4401 if (vmobj.routes.hasOwnProperty(key)) { 4402 routes[key] = vmobj.routes[key]; 4403 if (payload.hasOwnProperty('remove_routes') 4404 && payload.remove_routes.indexOf(key) !== -1) { 4405 4406 // in the remove_* list, don't load it. 4407 delete routes[key]; 4408 } 4409 } 4410 } 4411 4412 for (key in payload.set_routes) { 4413 if (payload.set_routes.hasOwnProperty(key)) { 4414 routes[key] = payload.set_routes[key]; 4415 } 4416 } 4417 4418 fs.writeFile(filename, JSON.stringify(routes, null, 2), 4419 function (err) { 4420 if (err) { 4421 callback(err); 4422 } else { 4423 log.debug('wrote routes to ' + filename); 4424 callback(); 4425 } 4426 }); 4427 } 4428 4429 function saveRoutes(payload, log, callback) 4430 { 4431 var protovm = {}; 4432 4433 assert(log, 'no logger passed to saveRoutes()'); 4434 4435 if (!payload.hasOwnProperty('zonepath') 4436 || !payload.hasOwnProperty('zpool') 4437 || !payload.hasOwnProperty('zonename')) { 4438 4439 callback(new Error('saveRoutes payload is missing zone ' 4440 + 'properties.')); 4441 return; 4442 } 4443 4444 protovm.zonepath = payload.zonepath; 4445 protovm.zpool = payload.zpool; 4446 protovm.zonename = payload.zonename; 4447 4448 if (payload.hasOwnProperty('routes')) { 4449 payload.set_routes = payload.routes; 4450 delete payload.routes; 4451 } 4452 4453 updateRoutes(protovm, payload, log, callback); 4454 } 4455 4456 function createVM(payload, log, callback) 4457 { 4458 assert(log, 'no logger passed to createVM()'); 4459 4460 async.series([ 4461 function (cb) { 4462 if (!payload.create_only) { 4463 // progress(2, 'checking required datasets'); 4464 checkDatasets(payload, log, cb); 4465 } else { 4466 cb(); 4467 } 4468 }, function (cb) { 4469 if (!payload.create_only) { 4470 // progress(29, 'creating volumes'); 4471 createVolumes(payload, log, cb); 4472 } else { 4473 cb(); 4474 } 4475 }, function (cb) { 4476 // progress(51, 'creating zone container'); 4477 createZone(payload, log, cb); 4478 } 4479 ], function (err, results) { 4480 if (err) { 4481 callback(err); 4482 } else { 4483 callback(null, results); 4484 } 4485 }); 4486 } 4487 4488 function fixZoneinitMetadataSock(zoneroot, log, callback) 4489 { 4490 var mdata_00; 4491 4492 // ensure we're safe to touch these files, zone should not be running here 4493 // so this just guards against malicious datasets. 4494 ['/var/zoneinit/includes', '/root/zoneinit.d'].forEach(function (dir) { 4495 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true}); 4496 }); 4497 4498 function replaceData(filename, cb) { 4499 fs.readFile(filename, 'utf8', function (error, data) { 4500 if (error) { 4501 log.error(error, 'failed to load 00-mdata.sh for replacement'); 4502 cb(error); 4503 return; 4504 } 4505 4506 data = data.replace(/\/var\/run\/smartdc\/metadata.sock/g, 4507 '/.zonecontrol/metadata.sock'); 4508 4509 log.trace('writing [' + data + '] to ' + filename); 4510 fs.writeFile(filename, data, 'utf8', function (err) { 4511 if (err) { 4512 log.error(err, 'failed to write ' + filename); 4513 } 4514 cb(err); 4515 }); 4516 }); 4517 } 4518 4519 // try /var/zoneinit/includes/00-mdata.sh first, since that's in new images 4520 mdata_00 = path.join(zoneroot, '/var/zoneinit/includes/00-mdata.sh'); 4521 fs.exists(mdata_00, function (exists1) { 4522 if (exists1) { 4523 log.info('fixing socket in /var/zoneinit/includes/00-mdata.sh'); 4524 replaceData(mdata_00, callback); 4525 } else { 4526 // didn't exist, so try location it exists in older images eg. 1.6.3 4527 mdata_00 = path.join(zoneroot, '/root/zoneinit.d/00-mdata.sh'); 4528 fs.exists(mdata_00, function (exists2) { 4529 if (exists2) { 4530 log.info('fixing socket in /root/zoneinit.d/00-mdata.sh'); 4531 replaceData(mdata_00, callback); 4532 } else { 4533 log.info('no 00-mdata.sh to cleanup in zoneinit'); 4534 callback(); 4535 } 4536 }); 4537 } 4538 }); 4539 } 4540 4541 function fixMdataFetchStart(zonepath, log, callback) 4542 { 4543 // svccfg validates zonepath 4544 var mdata_fetch_start = '/lib/svc/method/mdata-fetch'; 4545 4546 svccfg(zonepath, ['-s', 'svc:/smartdc/mdata:fetch', 'setprop', 'start/exec', 4547 '=', mdata_fetch_start], log, function (error, stdio) { 4548 4549 if (error) { 4550 log.error(error, 'failed to set mdata:fetch start method'); 4551 } else { 4552 log.info('successfully set mdata:fetch start method'); 4553 } 4554 4555 callback(error); 4556 }); 4557 } 4558 4559 function cleanupMessyDataset(zonepath, brand, log, callback) 4560 { 4561 var command; 4562 var zoneroot = path.join(zonepath, '/root'); 4563 4564 assert(log, 'no logger passed to cleanupMessyDataset()'); 4565 4566 try { 4567 ['/var/adm', '/var/svc/log', '/var/svc/manifest', '/root/zoneinit.d'] 4568 .forEach(function (dir) { 4569 4570 // This will ensure these are safe if they exist. 4571 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true}); 4572 }); 4573 } catch (e) { 4574 log.error(e, 'Unable to cleanup dataset: ' + e.message); 4575 callback(e); 4576 return; 4577 } 4578 4579 // We've verified the directories here exist, and have no symlinks in the 4580 // path (or don't exist) so rm -f <dir>/<file> should be safe regardless of 4581 // the type of <file> 4582 4583 command = 'rm -f ' 4584 + zoneroot + '/var/adm/utmpx ' 4585 + zoneroot + '/var/adm/wtmpx ' 4586 + zoneroot + '/var/svc/log/*.log ' 4587 + zoneroot + '/var/svc/mdata ' 4588 + zoneroot + '/var/svc/manifest/mdata.xml '; 4589 4590 if (! BRAND_OPTIONS[brand].features.zoneinit) { 4591 // eg. joyent-minimal (don't need zoneinit) 4592 command = command + zoneroot + '/root/zoneinit.xml ' 4593 + zoneroot + '/root/zoneinit ' 4594 + '&& rm -rf ' + zoneroot + '/root/zoneinit.d '; 4595 } 4596 4597 command = command + '&& touch ' + zoneroot + '/var/adm/wtmpx'; 4598 log.debug(command); 4599 exec(command, function (error, stdout, stderr) { 4600 log.debug({err: error, stdout: stdout, stderr: stderr}, 4601 'returned from cleaning up dataset'); 4602 if (error || !BRAND_OPTIONS[brand].features.zoneinit) { 4603 // either we already failed or this zone doesn't use zoneinit so 4604 // we don't need to bother fixing zoneinit's scripts. 4605 callback(error); 4606 } else { 4607 fixZoneinitMetadataSock(zoneroot, log, function (err) { 4608 // See OS-2314, currently we assume all zones w/ zoneinit also 4609 // have broken mdata:fetch when images are created from them. 4610 // Attempt to fix that too. 4611 fixMdataFetchStart(zonepath, log, callback); 4612 }); 4613 } 4614 }); 4615 } 4616 4617 // Helper for unlinking and replacing a file that you've already confirmed 4618 // has no symlinks. Throws error when fs.writeFileSync does, or when 4619 // fs.unlinkSync throws non ENOENT. 4620 function replaceFile(zoneroot, filename, data) { 4621 // first delete, in case file itself is a link 4622 try { 4623 fs.unlinkSync(path.join(zoneroot, filename)); 4624 } catch (e) { 4625 if (e.code !== 'ENOENT') { 4626 throw e; 4627 } 4628 } 4629 4630 fs.writeFileSync(path.join(zoneroot, filename), data); 4631 } 4632 4633 // NOTE: we write these out initially before the zone is started, but after that 4634 // rely on mdata-fetch in the zone to do the updates since we can't safely write 4635 // these files in the zones. 4636 function writeZoneNetfiles(payload, log, callback) 4637 { 4638 var hostname; 4639 var n; 4640 var nic; 4641 var primary_found = false; 4642 var zoneroot; 4643 4644 assert(log, 'no logger passed to writeZoneNetfiles()'); 4645 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload'); 4646 4647 zoneroot = payload.zonepath + '/root'; 4648 4649 try { 4650 assertSafeZonePath(zoneroot, '/etc', {type: 'dir', enoent_ok: true}); 4651 } catch (e) { 4652 log.error(e, 'Unable to write zone net files: ' + e.message); 4653 callback(e); 4654 return; 4655 } 4656 4657 log.info('Writing network files to zone root'); 4658 4659 try { 4660 for (nic in payload.add_nics) { 4661 if (payload.add_nics.hasOwnProperty(nic)) { 4662 n = payload.add_nics[nic]; 4663 4664 if (n.ip != 'dhcp') { 4665 replaceFile(zoneroot, '/etc/hostname.' 4666 + n.interface, n.ip + ' netmask ' + n.netmask 4667 + ' up' + '\n'); 4668 } 4669 4670 if (n.hasOwnProperty('primary') && !primary_found) { 4671 // only allow one primary network 4672 primary_found = true; 4673 if (n.hasOwnProperty('gateway')) { 4674 replaceFile(zoneroot, '/etc/defaultrouter', 4675 n.gateway + '\n'); 4676 } 4677 if (n.ip == 'dhcp') { 4678 replaceFile(zoneroot, '/etc/dhcp.' + n.interface, ''); 4679 } 4680 } 4681 } 4682 } 4683 4684 // It's possible we don't have zonename or hostname set because of the 4685 // ordering of adding the UUID. In any case, we'll have at least a uuid 4686 // here. 4687 if (payload.hasOwnProperty('hostname')) { 4688 hostname = payload.hostname; 4689 } else if (payload.hasOwnProperty('zonename')) { 4690 hostname = payload.zonename; 4691 } else { 4692 hostname = payload.uuid; 4693 } 4694 4695 replaceFile(zoneroot, '/etc/nodename', hostname + '\n'); 4696 } catch (e) { 4697 log.error(e, 'Unable to write zone networking files: ' + e.message); 4698 callback(e); 4699 return; 4700 } 4701 4702 callback(); 4703 } 4704 4705 /* 4706 * NOTE: once we no longer support old datasets that need the 'zoneconfig' file, 4707 * this function and calls to it can be removed. 4708 * 4709 * This writes out the zoneconfig file that is used by the zoneinit service in 4710 * joyent branded zones' datasets. 4711 * 4712 */ 4713 function writeZoneconfig(payload, log, callback) 4714 { 4715 var data; 4716 var hostname; 4717 var n; 4718 var nic; 4719 var zoneroot; 4720 4721 assert(log, 'no logger passed to writeZoneconfig()'); 4722 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload'); 4723 4724 zoneroot = payload.zonepath + '/root'; 4725 4726 log.info('Writing config for zoneinit'); 4727 4728 if (payload.hasOwnProperty('hostname')) { 4729 hostname = payload.hostname; 4730 } else { 4731 hostname = payload.zonename; 4732 } 4733 4734 data = 'TEMPLATE_VERSION=0.0.1\n' 4735 + 'ZONENAME=' + payload.zonename + '\n' 4736 + 'HOSTNAME=' + hostname + '.' + payload.dns_domain + '\n' 4737 + 'TMPFS=' + payload.tmpfs + 'm\n'; 4738 4739 if (payload.hasOwnProperty('add_nics') && payload.add_nics[0]) { 4740 4741 if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') { 4742 data = data + 'PUBLIC_IP=' + payload.add_nics[0].ip + '\n'; 4743 } 4744 if (payload.add_nics[1] && payload.add_nics[1].ip != 'dhcp') { 4745 data = data + 'PRIVATE_IP=' + payload.add_nics[1].ip + '\n'; 4746 } else if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') { 4747 // zoneinit uses private_ip for /etc/hosts, we want to 4748 // make that same as public, if there's no actual private. 4749 data = data + 'PRIVATE_IP=' + payload.add_nics[0].ip + '\n'; 4750 } 4751 } 4752 4753 if (payload.hasOwnProperty('resolvers')) { 4754 // zoneinit appends to resolv.conf rather than overwriting, so just 4755 // add to the zoneconfig and let zoneinit handle it 4756 data = data + 'RESOLVERS="' + payload.resolvers.join(' ') + '"\n'; 4757 } 4758 4759 for (nic in payload.add_nics) { 4760 if (payload.add_nics.hasOwnProperty(nic)) { 4761 n = payload.add_nics[nic]; 4762 data = data + n.interface.toUpperCase() + '_MAC=' + n.mac + '\n' 4763 + n.interface.toUpperCase() + '_INTERFACE=' 4764 + n.interface.toUpperCase() + '\n'; 4765 4766 if (n.ip != 'dhcp') { 4767 data = data + n.interface.toUpperCase() + '_IP=' + n.ip + '\n' 4768 + n.interface.toUpperCase() + '_NETMASK=' 4769 + n.netmask + '\n'; 4770 } 4771 } 4772 } 4773 4774 try { 4775 assertSafeZonePath(zoneroot, '/var/svc/log/system-zoneinit:default.log', 4776 {type: 'file', enoent_ok: true}); 4777 assertSafeZonePath(zoneroot, '/root/zoneconfig', 4778 {type: 'file', enoent_ok: true}); 4779 4780 replaceFile(zoneroot, '/var/svc/log/system-zoneinit:default.log', ''); 4781 4782 log.debug('writing zoneconfig ' + JSON.stringify(data) + ' to ' 4783 + zoneroot); 4784 4785 replaceFile(zoneroot, '/root/zoneconfig', data); 4786 callback(); 4787 } catch (e) { 4788 log.error(e, 'Unable to write zoneconfig files: ' + e.message); 4789 callback(e); 4790 return; 4791 } 4792 } 4793 4794 function zonecfg(args, log, callback) 4795 { 4796 var cmd = '/usr/sbin/zonecfg'; 4797 4798 assert(log, 'no logger passed to zonecfg()'); 4799 4800 log.debug(cmd + ' ' + args.join(' ')); 4801 execFile(cmd, args, function (error, stdout, stderr) { 4802 if (error) { 4803 callback(error, {'stdout': stdout, 'stderr': stderr}); 4804 } else { 4805 callback(null, {'stdout': stdout, 'stderr': stderr}); 4806 } 4807 }); 4808 } 4809 4810 function zonecfgFile(data, args, log, callback) 4811 { 4812 var tmpfile = '/tmp/zonecfg.' + process.pid + '.tmp'; 4813 4814 assert(log, 'no logger passed to zonecfgFile()'); 4815 4816 fs.writeFile(tmpfile, data, function (err, result) { 4817 if (err) { 4818 // On failure we don't delete the tmpfile so we can debug it. 4819 callback(err); 4820 } else { 4821 args.push('-f'); 4822 args.push(tmpfile); 4823 4824 zonecfg(args, log, function (e, fds) { 4825 if (e) { 4826 // keep temp file around for investigation 4827 callback(e, fds); 4828 } else { 4829 fs.unlink(tmpfile, function () { 4830 callback(null, fds); 4831 }); 4832 } 4833 }); 4834 } 4835 }); 4836 } 4837 4838 function zoneadm(args, log, callback) 4839 { 4840 var cmd = '/usr/sbin/zoneadm'; 4841 4842 assert(log, 'no logger passed to zoneadm()'); 4843 4844 log.debug(cmd + ' ' + args.join(' ')); 4845 execFile(cmd, args, function (error, stdout, stderr) { 4846 if (error) { 4847 callback(error, {'stdout': stdout, 'stderr': stderr}); 4848 } else { 4849 callback(null, {'stdout': stdout, 'stderr': stderr}); 4850 } 4851 }); 4852 } 4853 4854 function zfs(args, log, callback) 4855 { 4856 var cmd = '/usr/sbin/zfs'; 4857 4858 assert(log, 'no logger passed to zfs()'); 4859 4860 log.debug(cmd + ' ' + args.join(' ')); 4861 execFile(cmd, args, function (error, stdout, stderr) { 4862 if (error) { 4863 callback(error, {'stdout': stdout, 'stderr': stderr}); 4864 } else { 4865 callback(null, {'stdout': stdout, 'stderr': stderr}); 4866 } 4867 }); 4868 } 4869 4870 exports.getSysinfo = function (args, options, callback) 4871 { 4872 var cmd = '/usr/bin/sysinfo'; 4873 var log; 4874 4875 // we used to allow just one argument (callback) and we also allow 2 args 4876 // (args, callback) so that options is optional. 4877 if (arguments.length === 1) { 4878 callback = arguments[0]; 4879 args = []; 4880 options = {}; 4881 } 4882 if (arguments.length === 2) { 4883 callback = arguments[1]; 4884 options = {}; 4885 } 4886 4887 ensureLogging(false); 4888 if (options.hasOwnProperty('log')) { 4889 log = options.log; 4890 } else { 4891 log = VM.log.child({action: 'getSysinfo'}); 4892 } 4893 4894 log.debug(cmd + ' ' + args.join(' ')); 4895 execFile(cmd, args, function (error, stdout, stderr) { 4896 var sysinfo; 4897 4898 if (error) { 4899 callback(error, {'stdout': stdout, 'stderr': stderr}); 4900 } else { 4901 try { 4902 sysinfo = JSON.parse(stdout.toString()); 4903 } catch (e) { 4904 sysinfo = {}; 4905 } 4906 callback(null, sysinfo); 4907 } 4908 }); 4909 }; 4910 4911 /* 4912 * This watches zone transitions and calls callback when specified 4913 * state is reached. Optionally you can set a timeout which will 4914 * call your callback when the timeout occurs whether the transition 4915 * has happened or not. 4916 * 4917 * payload needs to have at least .zonename and .uuid 4918 * 4919 */ 4920 exports.waitForZoneState = function (payload, state, options, callback) 4921 { 4922 var log; 4923 var sysevent_state; 4924 var timeout; 4925 var timeout_secs = PROVISION_TIMEOUT; 4926 var watcher; 4927 4928 // options is optional 4929 if (arguments.length === 3) { 4930 callback = arguments[2]; 4931 options = {}; 4932 } 4933 4934 ensureLogging(false); 4935 if (options.hasOwnProperty('log')) { 4936 log = options.log; 4937 } else { 4938 log = VM.log.child({action: 'waitForZoneState', vm: payload.uuid}); 4939 } 4940 4941 if (options.hasOwnProperty('timeout')) { 4942 timeout_secs = options.timeout; 4943 } 4944 4945 sysevent_state = state; 4946 if (state === 'installed') { 4947 // Apparently the zone status 'installed' equals sysevent status 4948 // 'uninitialized' 4949 sysevent_state = 'uninitialized'; 4950 } 4951 4952 function done() { 4953 if (timeout) { 4954 clearTimeout(timeout); 4955 timeout = null; 4956 } 4957 } 4958 4959 function handler(err, obj) { 4960 if (err) { 4961 done(); 4962 callback(err); 4963 return; 4964 } 4965 log.trace('handler got: ' + JSON.stringify(obj)); 4966 if (obj.zonename !== payload.zonename) { 4967 return; 4968 } 4969 4970 if (obj.newstate === sysevent_state) { 4971 // Load again to confirm 4972 VM.lookup({'zonename': obj.zonename}, 4973 {fields: ['zone_state'], log: log}, 4974 function (error, res) { 4975 var handler_retry; 4976 4977 if (error) { 4978 watcher.cleanup(); 4979 done(); 4980 callback(error); 4981 return; 4982 } 4983 4984 if (res.length !== 1) { 4985 watcher.cleanup(); 4986 done(); 4987 callback(new Error('lookup could no find VM ' 4988 + obj.zonename)); 4989 return; 4990 } 4991 4992 if (res[0].hasOwnProperty('zone_state') 4993 && res[0].zone_state === state) { 4994 4995 // found the state we're looking for, success! 4996 log.debug('saw zone go to ' + obj.newstate + ' (' 4997 + state + ') calling callback()'); 4998 watcher.cleanup(); 4999 done(); 5000 callback(); 5001 } else if (timeout) { 5002 // we saw a state change to a state we don't care about 5003 // so if we've not timed out try reloading again in a 5004 // second. 5005 if (!handler_retry) { 5006 handler_retry = setTimeout(function () { 5007 if (timeout) { 5008 // try again if wait timeout is still set 5009 handler(null, obj); 5010 } 5011 handler_retry = null; 5012 }, 1000); 5013 log.debug('zone state after lookup: ' 5014 + res[0].zone_state + ', still waiting'); 5015 } else { 5016 log.debug('zone in wrong state but we already' 5017 + ' have a handler running'); 5018 } 5019 } else { 5020 // no timeout set and we're not at the correct state 5021 log.error('failed to reach state: ' + state); 5022 callback(new Error('failed to reach state: ' + state)); 5023 } 5024 } 5025 ); 5026 } 5027 } 5028 5029 watcher = watchZoneTransitions(handler, log); 5030 5031 timeout = setTimeout(function () { 5032 var err; 5033 5034 done(); 5035 watcher.cleanup(); 5036 err = new Error('timed out waiting for zone to transition to ' + state); 5037 err.code = 'ETIMEOUT'; 5038 callback(err); 5039 }, timeout_secs * 1000); 5040 5041 // after we've started the watcher (if we checked before there'd be a race) 5042 // we check whether we're already in the target state, if we are close it 5043 // down and return. 5044 VM.load(payload.uuid, {fields: ['zone_state'], log: log}, 5045 function (err, obj) { 5046 5047 if (err) { 5048 watcher.cleanup(); 5049 done(); 5050 callback(err); 5051 } else if (obj.hasOwnProperty('zone_state') 5052 && obj.zone_state === state) { 5053 5054 watcher.cleanup(); 5055 done(); 5056 log.info('VM is in state ' + state); 5057 callback(); // at correct state! 5058 } 5059 }); 5060 }; 5061 5062 // handler() will be called with an object describing the transition for any 5063 // transitions seen (after any filtering). The only filtering here is to remove 5064 // duplicate events. Other filtering should be done by the caller. 5065 function watchZoneTransitions(handler, log) { 5066 var buffer = ''; 5067 var chunks; 5068 var cleanup; 5069 var watcher; 5070 var watcher_pid; 5071 5072 assert(log, 'no logger passed to watchZoneTransitions()'); 5073 5074 if (!zoneevent) { 5075 5076 zoneevent = new EventEmitter(); 5077 5078 log.debug('/usr/vm/sbin/zoneevent'); 5079 watcher = spawn('/usr/vm/sbin/zoneevent', [], 5080 {'customFds': [-1, -1, -1]}); 5081 log.debug('zoneevent running with pid ' + watcher.pid); 5082 watcher_pid = watcher.pid; 5083 5084 watcher.stdout.on('data', function (data) { 5085 var chunk; 5086 var obj; 5087 var prev_msg; 5088 5089 buffer += data.toString(); 5090 chunks = buffer.split('\n'); 5091 while (chunks.length > 1) { 5092 chunk = chunks.shift(); 5093 obj = JSON.parse(chunk); 5094 5095 if (obj === prev_msg) { 5096 // Note: sometimes sysevent emits multiple events for the 5097 // same status, we only want the first one here because just 5098 // because sysevent does it, doesn't make it right. 5099 log.debug('duplicate zoneevent message! ' 5100 + JSON.stringify(obj)); 5101 } else if (zoneevent) { 5102 zoneevent.emit('zoneevent', null, obj); 5103 } 5104 } 5105 buffer = chunks.pop(); 5106 }); 5107 5108 // doesn't take input. 5109 watcher.stdin.end(); 5110 5111 watcher.on('exit', function (code) { 5112 log.warn('zoneevent watcher ' + watcher_pid + ' exited: ', 5113 JSON.stringify(code)); 5114 // tell all the listeners of this zoneevent (if there are any) that 5115 // we exited. Then null it out so next time we'll make a new one. 5116 zoneevent.emit('zoneevent', new Error('zoneevent watcher exited ' 5117 + 'prematurely with code: ' + code)); 5118 zoneevent = null; 5119 }); 5120 } 5121 5122 cleanup = function () { 5123 var listeners; 5124 5125 if (zoneevent) { 5126 listeners = zoneevent.listeners('zoneevent'); 5127 5128 log.debug('cleanup called w/ listeners: ' 5129 + util.inspect(listeners)); 5130 zoneevent.removeListener('zoneevent', handler); 5131 if (zoneevent.listeners('zoneevent').length === 0) { 5132 log.debug('zoneevent watcher ' + watcher_pid 5133 + ' cleanup called'); 5134 zoneevent = null; 5135 if (watcher) { 5136 watcher.stdout.destroy(); // so we don't send more 'data' 5137 watcher.stderr.destroy(); 5138 watcher.removeAllListeners('exit'); // so don't fail on kill 5139 watcher.kill(); 5140 watcher = null; 5141 } 5142 } 5143 } else if (watcher) { 5144 watcher.stdout.destroy(); // so we don't send more 'data' 5145 watcher.stderr.destroy(); 5146 watcher.removeAllListeners('exit'); // so don't fail on our kill 5147 watcher.kill(); 5148 watcher = null; 5149 } 5150 }; 5151 5152 zoneevent.on('zoneevent', handler); 5153 5154 return ({'cleanup': cleanup}); 5155 } 5156 5157 function fixPayloadMemory(payload, vmobj, log) 5158 { 5159 var brand; 5160 var max_locked; 5161 var max_phys; 5162 var min_overhead; 5163 var ram; 5164 5165 assert(log, 'no logger passed to fixPayloadMemory()'); 5166 5167 if (vmobj.hasOwnProperty('brand')) { 5168 brand = vmobj.brand; 5169 } else if (payload.hasOwnProperty('brand')) { 5170 brand = payload.brand; 5171 } 5172 5173 if (BRAND_OPTIONS[brand].features.default_memory_overhead 5174 && payload.hasOwnProperty('ram') 5175 && !payload.hasOwnProperty('max_physical_memory')) { 5176 5177 // For now we add overhead to the memory caps for KVM zones, this 5178 // is for the qemu process itself. Since customers don't have direct 5179 // access to zone memory, this exists mostly to protect against bugs. 5180 payload.max_physical_memory = (payload.ram 5181 + BRAND_OPTIONS[brand].features.default_memory_overhead); 5182 } else if (payload.hasOwnProperty('ram') 5183 && !payload.hasOwnProperty('max_physical_memory')) { 5184 5185 payload.max_physical_memory = payload.ram; 5186 } 5187 5188 if (payload.hasOwnProperty('max_physical_memory')) { 5189 if (!payload.hasOwnProperty('max_locked_memory')) { 5190 if (vmobj.hasOwnProperty('max_locked_memory') 5191 && vmobj.hasOwnProperty('max_physical_memory')) { 5192 5193 // we don't have a new value, so first try to keep the same 5194 // delta that existed before btw. max_phys and max_locked 5195 payload.max_locked_memory = payload.max_physical_memory 5196 - (vmobj.max_physical_memory - vmobj.max_locked_memory); 5197 } else { 5198 // existing obj doesn't have max_locked, add one now 5199 payload.max_locked_memory = payload.max_physical_memory; 5200 } 5201 } 5202 5203 if (!payload.hasOwnProperty('max_swap')) { 5204 if (vmobj.hasOwnProperty('max_swap') 5205 && vmobj.hasOwnProperty('max_physical_memory')) { 5206 5207 // we don't have a new value, so first try to keep the same 5208 // delta that existed before btw. max_phys and max_swap 5209 if (vmobj.max_swap === MINIMUM_MAX_SWAP 5210 && vmobj.max_swap <= MINIMUM_MAX_SWAP 5211 && payload.max_physical_memory >= MINIMUM_MAX_SWAP) { 5212 // in this case we artificially inflated before to meet 5213 // minimum tie back to ram. 5214 payload.max_swap = payload.max_physical_memory; 5215 } else { 5216 payload.max_swap = payload.max_physical_memory 5217 + (vmobj.max_swap - vmobj.max_physical_memory); 5218 } 5219 } else { 5220 // existing obj doesn't have max_swap, add one now 5221 payload.max_swap = payload.max_physical_memory; 5222 } 5223 5224 // never add a max_swap less than MINIMUM_MAX_SWAP 5225 if (payload.max_swap < MINIMUM_MAX_SWAP) { 5226 payload.max_swap = MINIMUM_MAX_SWAP; 5227 } 5228 } 5229 } 5230 5231 // if we're updating tmpfs it must be lower than our new max_physical or 5232 // if we're not also changing max_physical, it must be lower than the 5233 // current one. 5234 if (payload.hasOwnProperty('tmpfs')) { 5235 if (payload.hasOwnProperty('max_physical_memory') 5236 && (Number(payload.tmpfs) 5237 > Number(payload.max_physical_memory))) { 5238 5239 payload.tmpfs = payload.max_physical_memory; 5240 } else if (Number(payload.tmpfs) 5241 > Number(vmobj.max_physical_memory)) { 5242 5243 payload.tmpfs = vmobj.max_physical_memory; 5244 } 5245 } 5246 5247 if (payload.hasOwnProperty('max_physical_memory') 5248 && BRAND_OPTIONS[brand].features.use_tmpfs 5249 && !payload.hasOwnProperty('tmpfs')) { 5250 5251 if (vmobj.hasOwnProperty('max_physical_memory') 5252 && vmobj.hasOwnProperty('tmpfs')) { 5253 5254 // change tmpfs to be the same ratio of ram as before 5255 payload.tmpfs = ((vmobj.tmpfs / vmobj.max_physical_memory) 5256 * payload.max_physical_memory); 5257 payload.tmpfs = Number(payload.tmpfs).toFixed(); 5258 } else { 5259 // tmpfs must be < max_physical_memory, if not: pretend it was 5260 payload.tmpfs = payload.max_physical_memory; 5261 } 5262 } 5263 5264 // now that we've possibly adjusted target values, lower/raise values to 5265 // satisify max/min. 5266 5267 min_overhead = BRAND_OPTIONS[brand].features.min_memory_overhead; 5268 if (min_overhead) { 5269 ram = payload.hasOwnProperty('ram') ? payload.ram : vmobj.ram; 5270 max_phys = payload.hasOwnProperty('max_physical_memory') 5271 ? payload.max_physical_memory : vmobj.max_physical_memory; 5272 max_locked = payload.hasOwnProperty('max_locked_memory') 5273 ? payload.max_locked_memory : vmobj.max_locked_memory; 5274 5275 if ((ram + min_overhead) > max_phys) { 5276 payload.max_physical_memory = (ram + min_overhead); 5277 } 5278 if ((ram + min_overhead) > max_locked) { 5279 payload.max_locked_memory = (ram + min_overhead); 5280 } 5281 } 5282 5283 if (payload.hasOwnProperty('max_locked_memory')) { 5284 if (payload.hasOwnProperty('max_physical_memory')) { 5285 if (payload.max_locked_memory > payload.max_physical_memory) { 5286 log.warn('max_locked_memory (' + payload.max_locked_memory 5287 + ') > max_physical_memory (' + payload.max_physical_memory 5288 + ') clamping to ' + payload.max_physical_memory); 5289 payload.max_locked_memory = payload.max_physical_memory; 5290 } 5291 } else if (vmobj.hasOwnProperty('max_physical_memory')) { 5292 // new payload doesn't have a max_physical, so clamp to vmobj's 5293 if (payload.max_locked_memory > vmobj.max_physical_memory) { 5294 log.warn('max_locked_memory (' + payload.max_locked_memory 5295 + ') > vm.max_physical_memory (' + vmobj.max_physical_memory 5296 + ') clamping to ' + vmobj.max_physical_memory); 5297 payload.max_locked_memory = vmobj.max_physical_memory; 5298 } 5299 } 5300 } 5301 5302 if (payload.hasOwnProperty('max_swap')) { 5303 if (payload.hasOwnProperty('max_physical_memory')) { 5304 if (payload.max_swap < payload.max_physical_memory) { 5305 log.warn('max_swap (' + payload.max_swap 5306 + ') < max_physical_memory (' + payload.max_physical_memory 5307 + ') raising to ' + payload.max_physical_memory); 5308 payload.max_swap = payload.max_physical_memory; 5309 } 5310 } else if (vmobj.hasOwnProperty('max_physical_memory')) { 5311 // new payload doesn't have a max_physical, so raise to vmobj's 5312 if (payload.max_swap < vmobj.max_physical_memory) { 5313 log.warn('max_swap (' + payload.max_swap 5314 + ') < vm.max_physical_memory (' + vmobj.max_physical_memory 5315 + ') raising to ' + vmobj.max_physical_memory); 5316 payload.max_swap = vmobj.max_physical_memory; 5317 } 5318 } 5319 } 5320 } 5321 5322 // generate a new UUID if payload doesn't have one (also ensures that this uuid 5323 // does not already belong to a zone). 5324 function createZoneUUID(payload, log, callback) 5325 { 5326 var uuid; 5327 5328 assert(log, 'no logger passed to createZoneUUID()'); 5329 5330 if (payload.hasOwnProperty('uuid')) { 5331 // Ensure that the uuid is not already used. 5332 getZoneRecords(null, log, function (err, records) { 5333 if (err) { 5334 callback(err); 5335 } else { 5336 if (records.hasOwnProperty(payload.uuid)) { 5337 callback(new Error('vm with UUID ' + payload.uuid 5338 + ' already exists.')); 5339 } else { 5340 callback(null, payload.uuid); 5341 } 5342 } 5343 }); 5344 } else { 5345 log.debug('/usr/bin/uuid -v 4'); 5346 execFile('/usr/bin/uuid', ['-v', '4'], function (err, stdout, stderr) { 5347 if (err) { 5348 callback(err); 5349 return; 5350 } 5351 5352 // chomp trailing spaces and newlines 5353 uuid = stdout.toString().replace(/\s+$/g, ''); 5354 payload.uuid = uuid; 5355 log.info('generated uuid ' + uuid + ' for new VM'); 5356 getZoneRecords(null, log, function (e, records) { 5357 if (e) { 5358 callback(e); 5359 } else { 5360 if (records.hasOwnProperty(payload.uuid)) { 5361 callback(new Error('vm with UUID ' + payload.uuid 5362 + 'already exists.')); 5363 } else { 5364 callback(null, payload.uuid); 5365 } 5366 } 5367 }); 5368 }); 5369 } 5370 } 5371 5372 function applyZoneDefaults(payload, log) 5373 { 5374 var allowed; 5375 var disk; 5376 var disks; 5377 var n; 5378 var nic; 5379 var nics; 5380 var zvol; 5381 5382 assert(log, 'no logger passed to applyZoneDefaults()'); 5383 5384 log.debug('applying zone defaults'); 5385 5386 if (!payload.hasOwnProperty('owner_uuid')) { 5387 // We assume that this all-zero uuid can be treated as 'admin' 5388 payload.owner_uuid = '00000000-0000-0000-0000-000000000000'; 5389 } 5390 5391 if (!payload.hasOwnProperty('autoboot')) { 5392 payload.autoboot = true; 5393 } 5394 5395 if (!payload.hasOwnProperty('brand')) { 5396 payload.brand = 'joyent'; 5397 } 5398 5399 if (!payload.hasOwnProperty('zpool')) { 5400 payload.zpool = 'zones'; 5401 } 5402 5403 if (!payload.hasOwnProperty('dns_domain')) { 5404 payload.dns_domain = 'local'; 5405 } 5406 5407 if (!payload.hasOwnProperty('cpu_shares')) { 5408 payload.cpu_shares = 100; 5409 } else { 5410 if (payload.cpu_shares > 65535) { 5411 log.info('capping cpu_shares at 64k (was: ' 5412 + payload.cpu_shares + ')'); 5413 payload.cpu_shares = 65535; // max is 64K 5414 } 5415 } 5416 5417 if (!payload.hasOwnProperty('zfs_io_priority')) { 5418 payload.zfs_io_priority = 100; 5419 } 5420 5421 if (!payload.hasOwnProperty('max_lwps')) { 5422 payload.max_lwps = 2000; 5423 } 5424 5425 // We need to set the RAM here because we use it as the default for 5426 // the max_physical_memory below. If we've set max_phys and we're not 5427 // KVM, we'll use that instead of ram anyway. 5428 if (!payload.hasOwnProperty('ram')) { 5429 payload.ram = 256; 5430 } 5431 5432 fixPayloadMemory(payload, {}, log); 5433 5434 allowed = BRAND_OPTIONS[payload.brand].allowed_properties; 5435 if (allowed.hasOwnProperty('vcpus') && !payload.hasOwnProperty('vcpus')) { 5436 payload.vcpus = 1; 5437 } 5438 5439 if (BRAND_OPTIONS[payload.brand].features.use_tmpfs 5440 && (!payload.hasOwnProperty('tmpfs') 5441 || (Number(payload.tmpfs) > Number(payload.max_physical_memory)))) { 5442 5443 payload.tmpfs = payload.max_physical_memory; 5444 } 5445 5446 if (!payload.hasOwnProperty('limit_priv')) { 5447 // note: the limit privs are going to be added to the brand and 5448 // shouldn't need to be set here by default when that's done. 5449 if (BRAND_OPTIONS[payload.brand].features.limit_priv) { 5450 payload.limit_priv 5451 = BRAND_OPTIONS[payload.brand].features.limit_priv.join(','); 5452 } else { 5453 payload.limit_priv = 'default'; 5454 } 5455 } 5456 5457 if (!payload.hasOwnProperty('quota')) { 5458 payload.quota = '10'; // in GiB 5459 } 5460 5461 if (!payload.hasOwnProperty('billing_id')) { 5462 payload.billing_id = '00000000-0000-0000-0000-000000000000'; 5463 } 5464 5465 if (payload.hasOwnProperty('add_disks')) { 5466 // update 5467 disks = payload.add_disks; 5468 } else if (payload.hasOwnProperty('disks')) { 5469 disks = payload.disks; 5470 } else { 5471 // no disks at all 5472 disks = []; 5473 } 5474 5475 for (disk in disks) { 5476 if (disks.hasOwnProperty(disk)) { 5477 zvol = disks[disk]; 5478 if (!zvol.hasOwnProperty('model') 5479 && payload.hasOwnProperty('disk_driver')) { 5480 5481 zvol.model = payload.disk_driver; 5482 } 5483 if (!zvol.hasOwnProperty('media')) { 5484 zvol.media = 'disk'; 5485 } 5486 } 5487 } 5488 5489 if (payload.hasOwnProperty('add_nics')) { 5490 // update 5491 nics = payload.add_nics; 5492 } else if (payload.hasOwnProperty('nics')) { 5493 nics = payload.nics; 5494 } else { 5495 // no disks at all 5496 nics = []; 5497 } 5498 5499 for (nic in nics) { 5500 if (nics.hasOwnProperty(nic)) { 5501 n = nics[nic]; 5502 if (!n.hasOwnProperty('model') 5503 && payload.hasOwnProperty('nic_driver')) { 5504 5505 n.model = payload.nic_driver; 5506 } 5507 } 5508 } 5509 } 5510 5511 function validRecordSize(candidate) 5512 { 5513 if (candidate < 512) { 5514 // too low 5515 return (false); 5516 } else if (candidate > 131072) { 5517 // too high 5518 return (false); 5519 } else if ((candidate & (candidate - 1)) !== 0) { 5520 // not a power of 2 5521 return (false); 5522 } 5523 5524 return (true); 5525 } 5526 5527 // This function gets called for both create and update to check that payload 5528 // properties are reasonable. If vmobj is null, create is assumed, otherwise 5529 // update is assumed. 5530 function checkPayloadProperties(payload, vmobj, log, callback) 5531 { 5532 var array_fields = [ 5533 'add_nics', 'update_nics', 'remove_nics', 5534 'add_disks', 'update_disks', 'remove_disks', 5535 'add_filesystems', 'update_filesystems', 'remove_filesystems' 5536 ]; 5537 var changed_nics = []; 5538 var current_ips = []; 5539 var current_macs = []; 5540 var current_primary_ips = []; 5541 var current_vrids = []; 5542 var disk; 5543 var dst; 5544 var field; 5545 var filesys; 5546 var i; 5547 var ips = []; 5548 var is_nic = false; 5549 var live_ok; 5550 var mac; 5551 var macs = []; 5552 var m; 5553 var n; 5554 var nic; 5555 var nics_result = {}; 5556 var nics_result_ordered = []; 5557 var nic_fields = ['add_nics', 'update_nics']; 5558 var only_vrrp_nics = true; 5559 var primary_nics; 5560 var prop; 5561 var props; 5562 var ram; 5563 var route; 5564 var routes_result = {}; 5565 var brand; 5566 var vrids = []; 5567 var zvol; 5568 5569 assert(log, 'no logger passed to checkPayloadProperties()'); 5570 5571 if (vmobj) { 5572 brand = vmobj.brand; 5573 } else if (payload.hasOwnProperty('brand')) { 5574 brand = payload.brand; 5575 } else { 5576 callback(new Error('unable to determine brand for VM')); 5577 } 5578 5579 /* check types of fields that should be arrays */ 5580 for (field in array_fields) { 5581 field = array_fields[field]; 5582 if (payload.hasOwnProperty(field) && ! Array.isArray(payload[field])) { 5583 callback(new Error(field + ' must be an array.')); 5584 return; 5585 } 5586 } 5587 5588 if (!vmobj) { 5589 // This is a CREATE 5590 5591 // These should have already been enforced 5592 if (payload.max_locked_memory > payload.max_physical_memory) { 5593 callback(new Error('max_locked_memory must be <= ' 5594 + 'max_physical_memory')); 5595 return; 5596 } 5597 if (payload.max_swap < payload.max_physical_memory) { 5598 callback(new Error('max_swap must be >= max_physical_memory')); 5599 return; 5600 } 5601 5602 // We used to use zone_path instead of zonepath, so accept that too. 5603 if (payload.hasOwnProperty('zone_path') 5604 && !payload.hasOwnProperty('zonepath')) { 5605 5606 payload.zonepath = payload.zone_path; 5607 delete payload.zone_path; 5608 } 5609 } else { 5610 // This is an UPDATE 5611 5612 // can't update disks of a running VM 5613 if (payload.hasOwnProperty('add_disks') 5614 || payload.hasOwnProperty('remove_disks')) { 5615 5616 if ((vmobj.state !== 'stopped') 5617 || (vmobj.state === 'provisioning' 5618 && vmobj.zone_state !== 'installed')) { 5619 5620 callback(new Error('updates to disks are only allowed when ' 5621 + 'state is "stopped", currently: ' + vmobj.state + ' (' 5622 + vmobj.zone_state + ')')); 5623 return; 5624 } 5625 } 5626 5627 // For update_disks we can update refreservation and compression values 5628 // while running. If there are other parameters to update though we'll 5629 // reject. 5630 if (payload.hasOwnProperty('update_disks')) { 5631 if ((vmobj.state !== 'stopped') 5632 || (vmobj.state === 'provisioning' 5633 && vmobj.zone_state !== 'installed')) { 5634 5635 live_ok = true; 5636 5637 payload.update_disks.forEach(function (d) { 5638 var key; 5639 var keys = Object.keys(d); 5640 5641 while ((keys.length > 0) && live_ok) { 5642 key = keys.pop(); 5643 if ([ 5644 'compression', 5645 'path', 5646 'refreservation' 5647 ].indexOf(key) === -1) { 5648 5649 // this key is not allowed! 5650 live_ok = false; 5651 } 5652 } 5653 }); 5654 5655 if (!live_ok) { 5656 callback(new Error('at least one specified update to disks ' 5657 + 'is only allowed when state is "stopped", currently: ' 5658 + vmobj.state + ' (' + vmobj.zonestate + ')')); 5659 return; 5660 } 5661 } 5662 } 5663 5664 // if there's a min_overhead we ensure values are higher than ram. 5665 if (BRAND_OPTIONS[brand].features.min_memory_overhead) { 5666 if (payload.hasOwnProperty('ram')) { 5667 ram = payload.ram; 5668 } else { 5669 ram = vmobj.ram; 5670 } 5671 5672 // ensure none of these is < ram 5673 if (payload.hasOwnProperty('max_physical_memory') 5674 && payload.max_physical_memory < ram) { 5675 5676 callback(new Error('vm.max_physical_memory (' 5677 + payload.max_physical_memory + ') cannot be lower than' 5678 + ' vm.ram (' + ram + ')')); 5679 return; 5680 } 5681 if (payload.hasOwnProperty('max_locked_memory') 5682 && payload.max_locked_memory < ram) { 5683 5684 callback(new Error('vm.max_locked_memory (' 5685 + payload.max_locked_memory + ') cannot be lower than' 5686 + ' vm.ram (' + ram + ')')); 5687 return; 5688 } 5689 // This should not be allowed anyway because max_swap will be raised 5690 // to match max_physical_memory if you set it lower. 5691 if (payload.hasOwnProperty('max_swap')) { 5692 if (payload.max_swap < ram) { 5693 callback(new Error('vm.max_swap (' 5694 + payload.max_swap + ') cannot be lower than' 5695 + ' vm.ram (' + ram + ')')); 5696 return; 5697 } else if (payload.max_swap < MINIMUM_MAX_SWAP) { 5698 callback(new Error('vm.max_swap (' 5699 + payload.max_swap + ') cannot be lower than ' 5700 + MINIMUM_MAX_SWAP + 'MiB')); 5701 return; 5702 } 5703 } 5704 } 5705 5706 /* 5707 * keep track of current IPs/MACs so we can make sure they're not being 5708 * duplicated. 5709 * 5710 */ 5711 for (nic in vmobj.nics) { 5712 nic = vmobj.nics[nic]; 5713 if (nic.hasOwnProperty('ip') && nic.ip !== 'dhcp') { 5714 current_ips.push(nic.ip); 5715 } 5716 if (nic.hasOwnProperty('mac')) { 5717 current_macs.push(nic.mac); 5718 } 5719 if (nic.hasOwnProperty('vrrp_vrid')) { 5720 current_vrids.push(nic.vrrp_vrid); 5721 } 5722 if (nic.hasOwnProperty('vrrp_primary_ip')) { 5723 current_primary_ips.push(nic.vrrp_primary_ip); 5724 } 5725 5726 if (nic.hasOwnProperty('mac') || nic.hasOwnProperty('vrrp_vrid')) { 5727 mac = nic.hasOwnProperty('mac') ? nic.mac 5728 : vrrpMAC(nic.vrrp_vrid); 5729 if (!nics_result.hasOwnProperty(mac)) { 5730 nics_result[mac] = nic; 5731 nics_result_ordered.push(nic); 5732 } 5733 } 5734 } 5735 5736 // Keep track of route additions / deletions, to make sure that 5737 // we're not setting link-local routes against nics that don't exist 5738 for (route in vmobj.routes) { 5739 routes_result[route] = vmobj.routes[route]; 5740 } 5741 } 5742 5743 if (payload.hasOwnProperty('add_disks')) { 5744 for (disk in payload.add_disks) { 5745 if (payload.add_disks.hasOwnProperty(disk)) { 5746 zvol = payload.add_disks[disk]; 5747 5748 // path is only allowed in 2 cases when adding a disk: 5749 // 5750 // 1) for cdrom devices 5751 // 2) when nocreate is specified 5752 // 5753 if (zvol.hasOwnProperty('path')) { 5754 if (zvol.media !== 'cdrom' && !zvol.nocreate) { 5755 callback(new Error('you cannot specify a path for a ' 5756 + 'disk unless you set nocreate=true')); 5757 return; 5758 } 5759 } 5760 5761 // NOTE: We'll have verified the .zpool argument is a valid 5762 // zpool using VM.validate() if it's set. 5763 5764 if (zvol.hasOwnProperty('block_size') 5765 && !validRecordSize(zvol.block_size)) { 5766 5767 callback(new Error('invalid .block_size(' + zvol.block_size 5768 + '), must be 512-131072 and a power of 2.')); 5769 return; 5770 } 5771 5772 if (zvol.hasOwnProperty('block_size') 5773 && zvol.hasOwnProperty('image_uuid')) { 5774 5775 callback(new Error('setting both .block_size and ' 5776 + '.image_uuid on a volume is invalid')); 5777 } 5778 5779 if (zvol.hasOwnProperty('compression')) { 5780 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) { 5781 callback(new Error('invalid compression setting for ' 5782 + 'disk, must be one of: ' 5783 + VM.COMPRESSION_TYPES.join(', '))); 5784 } 5785 } 5786 5787 if (!zvol.hasOwnProperty('model') 5788 || zvol.model === 'undefined') { 5789 5790 if (vmobj && vmobj.hasOwnProperty('disk_driver')) { 5791 zvol.model = vmobj.disk_driver; 5792 log.debug('set model to ' + zvol.model 5793 + ' from disk_driver'); 5794 } else if (vmobj && vmobj.hasOwnProperty('disks') 5795 && vmobj.disks.length > 0 && vmobj.disks[0].model) { 5796 5797 zvol.model = vmobj.disks[0].model; 5798 log.debug('set model to ' + zvol.model + ' from disk0'); 5799 } else { 5800 callback(new Error('missing .model option for ' 5801 + 'disk: ' + JSON.stringify(zvol))); 5802 return; 5803 } 5804 } else if (VM.DISK_MODELS.indexOf(zvol.model) === -1) { 5805 callback(new Error('"' + zvol.model + '"' 5806 + ' is not a valid disk model. Valid are: ' 5807 + VM.DISK_MODELS.join(','))); 5808 return; 5809 } 5810 } 5811 } 5812 } 5813 5814 if (payload.hasOwnProperty('update_disks')) { 5815 for (disk in payload.update_disks) { 5816 if (payload.update_disks.hasOwnProperty(disk)) { 5817 zvol = payload.update_disks[disk]; 5818 5819 if (zvol.hasOwnProperty('compression')) { 5820 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) { 5821 callback(new Error('invalid compression type for ' 5822 + 'disk, must be one of: ' 5823 + VM.COMPRESSION_TYPES.join(', '))); 5824 } 5825 } 5826 5827 if (zvol.hasOwnProperty('block_size')) { 5828 callback(new Error('cannot change .block_size for a disk ' 5829 + 'after creation')); 5830 return; 5831 } 5832 } 5833 } 5834 } 5835 5836 // If we're receiving, we might not have the filesystem yet 5837 if (!payload.hasOwnProperty('transition') 5838 || payload.transition.transition !== 'receiving') { 5839 5840 for (filesys in payload.filesystems) { 5841 filesys = payload.filesystems[filesys]; 5842 if (!fs.existsSync(filesys.source)) { 5843 callback(new Error('missing requested filesystem: ' 5844 + filesys.source)); 5845 return; 5846 } 5847 } 5848 } 5849 5850 if (payload.hasOwnProperty('default_gateway') 5851 && payload.default_gateway !== '') { 5852 5853 log.warn('DEPRECATED: default_gateway should no longer be used, ' 5854 + 'instead set one NIC primary and use nic.gateway.'); 5855 } 5856 5857 primary_nics = 0; 5858 for (field in nic_fields) { 5859 field = nic_fields[field]; 5860 if (payload.hasOwnProperty(field)) { 5861 for (nic in payload[field]) { 5862 if (payload[field].hasOwnProperty(nic)) { 5863 n = payload[field][nic]; 5864 5865 // MAC will always conflict in update, since that's the key 5866 if (field === 'add_nics' && n.hasOwnProperty('mac')) { 5867 if ((macs.indexOf(n.mac) !== -1) 5868 || current_macs.indexOf(n.mac) !== -1) { 5869 5870 callback(new Error('Cannot add multiple NICs with ' 5871 + 'the same MAC: ' + n.mac)); 5872 return; 5873 } 5874 macs.push(n.mac); 5875 } 5876 5877 if (field === 'add_nics' || field === 'update_nics') { 5878 if (n.hasOwnProperty('primary')) { 5879 if (n.primary !== true) { 5880 callback(new Error('invalid value for NIC\'s ' 5881 + 'primary flag: ' + n.primary + ' (must be' 5882 + ' true)')); 5883 return; 5884 } 5885 primary_nics++; 5886 } 5887 changed_nics.push(n); 5888 } 5889 5890 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') { 5891 if (ips.indexOf(n.ip) !== -1 5892 || current_ips.indexOf(n.ip) !== -1) { 5893 5894 callback(new Error('Cannot add multiple NICs with ' 5895 + 'the same IP: ' + n.ip)); 5896 return; 5897 } 5898 ips.push(n.ip); 5899 } 5900 5901 if (n.hasOwnProperty('vrrp_vrid')) { 5902 if (current_vrids.indexOf(n.vrrp_vrid) !== -1 5903 || vrids.indexOf(n.vrrp_vrid) !== -1) { 5904 callback(new Error('Cannot add multiple NICs with ' 5905 + 'the same VRID: ' + n.vrrp_vrid)); 5906 return; 5907 } 5908 vrids.push(n.vrrp_vrid); 5909 } 5910 5911 if (field === 'add_nics' 5912 && n.hasOwnProperty('vrrp_vrid') 5913 && n.hasOwnProperty('mac')) { 5914 callback( 5915 new Error('Cannot set both mac and vrrp_vrid')); 5916 return; 5917 } 5918 5919 if (n.hasOwnProperty('vrrp_primary_ip')) { 5920 current_primary_ips.push(n.vrrp_primary_ip); 5921 } 5922 5923 if (BRAND_OPTIONS[brand].features.model_required 5924 && field === 'add_nics' 5925 && (!n.hasOwnProperty('model') || !n.model 5926 || n.model === 'undefined' || n.model.length === 0)) { 5927 5928 5929 if (vmobj && vmobj.hasOwnProperty('nic_driver')) { 5930 n.model = vmobj.nic_driver; 5931 log.debug('set model to ' + n.model 5932 + ' from nic_driver'); 5933 } else if (vmobj && vmobj.hasOwnProperty('nics') 5934 && vmobj.nics.length > 0 && vmobj.nics[0].model) { 5935 5936 n.model = vmobj.nics[0].model; 5937 log.debug('set model to ' + n.model + ' from nic0'); 5938 } else { 5939 callback(new Error('missing .model option for NIC: ' 5940 + JSON.stringify(n))); 5941 return; 5942 } 5943 } 5944 5945 if (field === 'add_nics' && n.ip !== 'dhcp' 5946 && (!n.hasOwnProperty('netmask') 5947 || !net.isIPv4(n.netmask))) { 5948 5949 callback(new Error('invalid or missing .netmask option ' 5950 + 'for NIC: ' + JSON.stringify(n))); 5951 return; 5952 } 5953 5954 if ((field === 'add_nics' || field === 'update_nics') 5955 && n.hasOwnProperty('ip') && n.ip !== 'dhcp' 5956 && !net.isIPv4(n.ip)) { 5957 5958 callback(new Error('invalid IP for NIC: ' 5959 + JSON.stringify(n))); 5960 return; 5961 } 5962 5963 if (field === 'add_nics' && (!n.hasOwnProperty('nic_tag') 5964 || !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/))) { 5965 5966 callback(new Error('invalid or missing .nic_tag option ' 5967 + 'for NIC: ' + JSON.stringify(n))); 5968 return; 5969 } 5970 5971 if (field === 'update_nics' && n.hasOwnProperty('model') 5972 && (!n.model || n.model === 'undefined' 5973 || n.model.length === 0)) { 5974 5975 callback(new Error('invalid .model option for NIC: ' 5976 + JSON.stringify(n))); 5977 return; 5978 } 5979 5980 if (field === 'update_nics' && n.hasOwnProperty('netmask') 5981 && (!n.netmask || !net.isIPv4(n.netmask))) { 5982 5983 callback(new Error('invalid .netmask option for NIC: ' 5984 + JSON.stringify(n))); 5985 return; 5986 } 5987 5988 if (field === 'update_nics' && n.hasOwnProperty('nic_tag') 5989 && !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/)) { 5990 5991 callback(new Error('invalid .nic_tag option for NIC: ' 5992 + JSON.stringify(n))); 5993 return; 5994 } 5995 5996 if (n.hasOwnProperty('mac') 5997 || n.hasOwnProperty('vrrp_vrid')) { 5998 mac = n.hasOwnProperty('mac') ? n.mac 5999 : vrrpMAC(n.vrrp_vrid); 6000 if (nics_result.hasOwnProperty(mac)) { 6001 var p; 6002 for (p in n) { 6003 nics_result[mac][p] = n[p]; 6004 } 6005 6006 nics_result_ordered.forEach(function (on) { 6007 if (on.hasOwnProperty('mac') && on.mac == mac) { 6008 for (p in n) { 6009 on[p] = n[p]; 6010 } 6011 } 6012 }); 6013 } else { 6014 nics_result[mac] = n; 6015 nics_result_ordered.push(n); 6016 } 6017 } 6018 6019 if ((field === 'add_nics' || field === 'update_nics') 6020 && n.hasOwnProperty('allowed_ips')) { 6021 try { 6022 validateIPlist(n.allowed_ips); 6023 } catch (ipListErr) { 6024 callback(ipListErr); 6025 return; 6026 } 6027 } 6028 } 6029 } 6030 } 6031 } 6032 6033 if (payload.hasOwnProperty('remove_nics')) { 6034 for (m in payload.remove_nics) { 6035 m = payload.remove_nics[m]; 6036 n = nics_result[m]; 6037 if (!n) { 6038 continue; 6039 } 6040 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') { 6041 i = ips.indexOf(n.ip); 6042 if (i !== -1) { 6043 ips.splice(i, 1); 6044 } 6045 i = current_ips.indexOf(n.ip); 6046 if (i !== -1) { 6047 current_ips.splice(i, 1); 6048 } 6049 } 6050 delete nics_result[m]; 6051 6052 for (i in nics_result_ordered) { 6053 n = nics_result_ordered[i]; 6054 if (n.hasOwnProperty('mac') && n.mac == m) { 6055 nics_result_ordered.splice(i, 1); 6056 break; 6057 } 6058 } 6059 } 6060 } 6061 6062 // nics_result now has the state of the nics after the update - now check 6063 // properties that depend on each other or on other nics 6064 for (n in nics_result) { 6065 n = nics_result[n]; 6066 if (n.hasOwnProperty('vrrp_vrid')) { 6067 if (n.hasOwnProperty('ip') 6068 && current_primary_ips.indexOf(n.ip) !== -1) { 6069 callback( 6070 new Error( 6071 'Cannot set vrrp_primary_ip to the IP of a VRRP nic')); 6072 return; 6073 } 6074 6075 if (!n.hasOwnProperty('vrrp_primary_ip')) { 6076 callback(new Error( 6077 'vrrp_vrid set but not vrrp_primary_ip')); 6078 return; 6079 } 6080 } else { 6081 only_vrrp_nics = false; 6082 } 6083 } 6084 6085 if (only_vrrp_nics && Object.keys(nics_result).length !== 0) { 6086 callback(new Error('VM cannot contain only VRRP nics')); 6087 return; 6088 } 6089 6090 for (i in current_primary_ips) { 6091 i = current_primary_ips[i]; 6092 if ((current_ips.indexOf(i) === -1) 6093 && (ips.indexOf(i) === -1)) { 6094 callback(new Error( 6095 'vrrp_primary_ip must belong to the same VM')); 6096 return; 6097 } 6098 } 6099 6100 // Since we always need a primary nic, don't allow a value other than true 6101 // for primary flag. Also ensure we're not trying to set primary for more 6102 // than one nic. 6103 if (primary_nics > 1) { 6104 callback(new Error('payload specifies more than 1 primary NIC')); 6105 return; 6106 } 6107 6108 if (payload.hasOwnProperty('vga') 6109 && VM.VGA_TYPES.indexOf(payload.vga) === -1) { 6110 6111 callback(new Error('Invalid VGA type: "' + payload.vga 6112 + '", supported types are: ' + VM.VGA_TYPES.join(','))); 6113 return; 6114 } 6115 6116 function validLocalRoute(r) { 6117 var nicIdx = r.match(/nics\[(\d+)\]/); 6118 if (!nicIdx) { 6119 is_nic = false; 6120 return false; 6121 } 6122 is_nic = true; 6123 6124 if (nics_result_ordered.length === 0) { 6125 return false; 6126 } 6127 6128 nicIdx = Number(nicIdx[1]); 6129 if (!nics_result_ordered[nicIdx] 6130 || !nics_result_ordered[nicIdx].hasOwnProperty('ip') 6131 || nics_result_ordered[nicIdx].ip === 'dhcp') { 6132 return false; 6133 } 6134 6135 return true; 6136 } 6137 6138 props = [ 'routes', 'set_routes' ]; 6139 for (prop in props) { 6140 prop = props[prop]; 6141 if (payload.hasOwnProperty(prop)) { 6142 for (dst in payload[prop]) { 6143 var src = payload[prop][dst]; 6144 6145 if (!net.isIPv4(dst) && !isCIDR(dst)) { 6146 callback(new Error('Invalid route destination: "' + dst 6147 + '" (must be IP address or CIDR)')); 6148 return; 6149 } 6150 6151 if (!net.isIPv4(src) && !validLocalRoute(src)) { 6152 callback(new Error( 6153 is_nic ? 'Route gateway: "' + src 6154 + '" refers to non-existent or DHCP nic' 6155 : 'Invalid route gateway: "' + src 6156 + '" (must be IP address or nic)')); 6157 return; 6158 } 6159 6160 routes_result[dst] = src; 6161 } 6162 } 6163 } 6164 6165 if (payload.hasOwnProperty('remove_routes')) { 6166 for (dst in payload.remove_routes) { 6167 dst = payload.remove_routes[dst]; 6168 delete routes_result[dst]; 6169 } 6170 } 6171 6172 // Now that we've applied all updates to routes, make sure that all 6173 // link-local routes refer to a nic that still exists 6174 for (dst in routes_result) { 6175 if (!net.isIPv4(routes_result[dst]) 6176 && !validLocalRoute(routes_result[dst])) { 6177 callback(new Error('Route gateway: "' + routes_result[dst] 6178 + '" refers to non-existent or DHCP nic')); 6179 return; 6180 } 6181 } 6182 6183 // Ensure password is not too long 6184 if (payload.hasOwnProperty('vnc_password') 6185 && payload.vnc_password.length > 8) { 6186 6187 callback(new Error('VNC password is too long, maximum length is 8 ' 6188 + 'characters.')); 6189 return; 6190 } 6191 6192 props = ['zfs_root_recsize', 'zfs_data_recsize']; 6193 for (prop in props) { 6194 prop = props[prop]; 6195 if (payload.hasOwnProperty(prop)) { 6196 if (payload[prop] === 0 || payload[prop] === '') { 6197 // this is the default, so set it back to that. 6198 payload[prop] = 131072; 6199 } else if (!validRecordSize(payload[prop])) { 6200 callback(new Error('invalid ' + prop + ' (' + payload[prop] 6201 + '), must be 512-131072 and a power of 2. ' 6202 + '(0 to disable)')); 6203 return; 6204 } 6205 } 6206 } 6207 props = ['zfs_root_compression', 'zfs_data_compression']; 6208 for (prop in props) { 6209 prop = props[prop]; 6210 6211 if (payload.hasOwnProperty(prop)) { 6212 if (VM.COMPRESSION_TYPES.indexOf(payload[prop]) === -1) { 6213 callback(new Error('invalid compression type for ' 6214 + payload[prop] + ', must be one of: ' 6215 + VM.COMPRESSION_TYPES.join(', '))); 6216 } 6217 } 6218 } 6219 6220 // Ensure MACs and IPs are not already used on this vm 6221 // NOTE: can't check other nodes yet. 6222 6223 async.series([ 6224 function (cb) { 6225 lookupConflicts(macs, ips, vrids, log, function (error, conflict) { 6226 if (error) { 6227 cb(error); 6228 } else { 6229 if (conflict) { 6230 cb(new Error('Conflict detected with another ' 6231 + 'vm, please check the MAC, IP, and VRID')); 6232 } else { 6233 log.debug('no conflicts'); 6234 cb(); 6235 } 6236 } 6237 }); 6238 }, function (cb) { 6239 lookupInvalidNicTags(changed_nics, log, function (e) { 6240 if (e) { 6241 cb(e); 6242 } else { 6243 cb(); 6244 } 6245 }); 6246 }, function (cb) { 6247 // We only allow adding firewall rules on create 6248 if (vmobj) { 6249 log.debug('update: not validating firewall data'); 6250 cb(); 6251 return; 6252 } 6253 6254 if (!payload.hasOwnProperty('firewall')) { 6255 log.debug('no firewall data in payload: not validating'); 6256 cb(); 6257 return; 6258 } 6259 validateFirewall(payload, log, cb); 6260 } 6261 ], function (err) { 6262 log.trace('leaving checkPayloadProperties()'); 6263 callback(err); 6264 }); 6265 } 6266 6267 function createDelegatedDataset(payload, log, callback) 6268 { 6269 var args; 6270 var ds; 6271 var zcfg = ''; 6272 6273 assert(log, 'no logger passed to createDelegatedDataset()'); 6274 6275 if (payload.delegate_dataset) { 6276 log.info('creating delegated dataset.'); 6277 if (!payload.hasOwnProperty('zfs_filesystem')) { 6278 callback(new Error('payload missing zfs_filesystem')); 6279 return; 6280 } 6281 ds = path.join(payload.zfs_filesystem, '/data'); 6282 6283 args = ['create']; 6284 if (payload.hasOwnProperty('zfs_data_compression')) { 6285 args.push('-o', 'compression=' + payload.zfs_data_compression); 6286 } 6287 if (payload.hasOwnProperty('zfs_data_recsize')) { 6288 args.push('-o', 'recsize=' + payload.zfs_data_recsize); 6289 } 6290 args.push(ds); 6291 6292 zfs(args, log, function (err) { 6293 if (err) { 6294 callback(err); 6295 return; 6296 } 6297 6298 zcfg = zcfg + 'add dataset; set name=' + ds + '; end\n'; 6299 zonecfg(['-u', payload.uuid, zcfg], log, function (e, fds) { 6300 if (e) { 6301 log.error({'err': e, stdout: fds.stdout, 6302 stderr: fds.stderr}, 'unable to add delegated dataset ' 6303 + ds + ' to ' + payload.uuid); 6304 callback(e); 6305 } else { 6306 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 6307 'added delegated dataset ' + ds); 6308 callback(); 6309 } 6310 }); 6311 }); 6312 } else { 6313 callback(); 6314 } 6315 } 6316 6317 function buildAddRemoveList(vmobj, payload, type, key, updatable) 6318 { 6319 var add = []; 6320 var add_key; 6321 var field; 6322 var newobj; 6323 var oldobj; 6324 var plural = type + 's'; 6325 var remove = []; 6326 var remove_key; 6327 var update_key; 6328 6329 // initialize some plurals 6330 add_key = 'add_' + plural; 6331 remove_key = 'remove_' + plural; 6332 update_key = 'update_' + plural; 6333 6334 // There's no way to update properties on a disk or nic with zonecfg 6335 // currently. Yes, really. So any disks/nics that should be updated, we 6336 // remove then add with the new properties. 6337 if (payload.hasOwnProperty(update_key)) { 6338 for (newobj in payload[update_key]) { 6339 newobj = payload[update_key][newobj]; 6340 for (oldobj in vmobj[plural]) { 6341 oldobj = vmobj[plural][oldobj]; 6342 6343 if (oldobj[key] === newobj[key]) { 6344 // This is the one to update: remove and add. 6345 remove.push(oldobj[key]); 6346 6347 // only some fields make sense to update. 6348 for (field in updatable) { 6349 field = updatable[field]; 6350 if (newobj.hasOwnProperty(field)) { 6351 oldobj[field] = newobj[field]; 6352 } 6353 } 6354 6355 add.push(oldobj); 6356 } 6357 } 6358 } 6359 } 6360 6361 if (payload.hasOwnProperty(remove_key)) { 6362 for (newobj in payload[remove_key]) { 6363 newobj = payload[remove_key][newobj]; 6364 remove.push(newobj); 6365 } 6366 } 6367 6368 if (payload.hasOwnProperty(add_key)) { 6369 for (newobj in payload[add_key]) { 6370 newobj = payload[add_key][newobj]; 6371 add.push(newobj); 6372 } 6373 } 6374 6375 return ({'add': add, 'remove': remove}); 6376 } 6377 6378 function buildDiskZonecfg(vmobj, payload) 6379 { 6380 var add = []; 6381 var disk; 6382 var lists; 6383 var remove = []; 6384 var zcfg = ''; 6385 6386 lists = buildAddRemoveList(vmobj, payload, 'disk', 'path', 6387 UPDATABLE_DISK_PROPS); 6388 remove = lists.remove; 6389 add = lists.add; 6390 6391 // remove is a list of disk paths, add a remove for each now. 6392 for (disk in remove) { 6393 disk = remove[disk]; 6394 zcfg = zcfg + 'remove -F device match=' + disk + '\n'; 6395 } 6396 6397 for (disk in add) { 6398 disk = add[disk]; 6399 6400 zcfg = zcfg + 'add device\n' 6401 + 'set match=' + disk.path + '\n' 6402 + 'add property (name=boot, value="' 6403 + (disk.boot ? 'true' : 'false') + '")\n' 6404 + 'add property (name=model, value="' + disk.model + '")\n'; 6405 6406 if (disk.hasOwnProperty('media')) { 6407 zcfg = zcfg 6408 + 'add property (name=media, value="' 6409 + disk.media + '")\n'; 6410 } 6411 6412 if (disk.hasOwnProperty('image_size')) { 6413 zcfg = zcfg 6414 + 'add property (name=image-size, value="' 6415 + disk.image_size + '")\n'; 6416 } else if (disk.hasOwnProperty('size')) { 6417 zcfg = zcfg + 'add property (name=size, value="' 6418 + disk.size + '")\n'; 6419 } 6420 6421 if (disk.hasOwnProperty('image_uuid')) { 6422 zcfg = zcfg 6423 + 'add property (name=image-uuid, value="' 6424 + disk.image_uuid + '")\n'; 6425 } 6426 6427 if (disk.hasOwnProperty('image_name')) { 6428 zcfg = zcfg + 'add property (name=image-name, value="' 6429 + disk.image_name + '")\n'; 6430 } 6431 6432 zcfg = zcfg + 'end\n'; 6433 } 6434 6435 return zcfg; 6436 } 6437 6438 function buildNicZonecfg(vmobj, payload) 6439 { 6440 var add; 6441 var lists; 6442 var matches; 6443 var n; 6444 var new_primary; 6445 var nic; 6446 var nic_idx = 0; 6447 var remove; 6448 var updated_primary; 6449 var used_nic_indexes = []; 6450 var zcfg = ''; 6451 6452 if (vmobj.hasOwnProperty('nics')) { 6453 // check whether we're adding or updating to set the primary flag. If we 6454 // are also find the existing NIC with the primary flag. If that's not 6455 // being removed, update it to remove the primary flag. 6456 if (payload.hasOwnProperty('add_nics')) { 6457 for (nic in payload.add_nics) { 6458 nic = payload.add_nics[nic]; 6459 if (nic.hasOwnProperty('primary')) { 6460 new_primary = nic.mac; 6461 } 6462 } 6463 } 6464 if (payload.hasOwnProperty('update_nics')) { 6465 for (nic in payload.update_nics) { 6466 nic = payload.update_nics[nic]; 6467 if (nic.hasOwnProperty('primary')) { 6468 new_primary = nic.mac; 6469 } 6470 } 6471 } 6472 if (new_primary) { 6473 // find old primary 6474 for (nic in vmobj.nics) { 6475 nic = vmobj.nics[nic]; 6476 if (nic.hasOwnProperty('primary') && nic.mac !== new_primary) { 6477 // we have a new primary, so un-primary the old. 6478 if (payload.hasOwnProperty('remove_nics') 6479 && payload.remove_nics.indexOf(nic.mac) !== -1) { 6480 6481 // we're removing the old primary so: done. 6482 break; 6483 } else if (payload.hasOwnProperty('update_nics')) { 6484 updated_primary = false; 6485 for (n in payload.update_nics) { 6486 n = payload.update_nics[n]; 6487 if (n.mac === nic.mac) { 6488 n.primary = false; 6489 updated_primary = true; 6490 } 6491 } 6492 if (!updated_primary) { 6493 payload.update_nics.push({'mac': nic.mac, 6494 'primary': false}); 6495 } 6496 } else { 6497 // just add a new update to unset the 6498 payload.update_nics = 6499 [ {'mac': nic.mac, 'primary': false} ]; 6500 } 6501 } 6502 } 6503 } 6504 } 6505 6506 lists = buildAddRemoveList(vmobj, payload, 'nic', 'mac', 6507 UPDATABLE_NIC_PROPS); 6508 remove = lists.remove; 6509 add = lists.add; 6510 6511 // create a list of used indexes so we can find the free ones 6512 if (vmobj.hasOwnProperty('nics')) { 6513 for (n in vmobj.nics) { 6514 if (vmobj.nics[n].hasOwnProperty('interface')) { 6515 matches = vmobj.nics[n].interface.match(/^net(\d+)$/); 6516 if (matches) { 6517 used_nic_indexes.push(Number(matches[1])); 6518 } 6519 } 6520 } 6521 } 6522 6523 // assign next available interface for nics without one 6524 for (nic in add) { 6525 nic = add[nic]; 6526 if (!nic.hasOwnProperty('interface')) { 6527 while (used_nic_indexes.indexOf(nic_idx) !== -1) { 6528 nic_idx++; 6529 } 6530 nic.interface = 'net' + nic_idx; 6531 used_nic_indexes.push(Number(nic_idx)); 6532 } 6533 6534 // Changing the VRID changes the MAC address too, since the VRID is 6535 // encoded in the MAC. This can't be done until after 6536 // buildAddRemoveList above, since mac is used as the key to figure 6537 // out which nic is which 6538 if (nic.hasOwnProperty('vrrp_vrid')) { 6539 nic.mac = vrrpMAC(nic.vrrp_vrid); 6540 } 6541 } 6542 6543 // remove is a list of nic macs, add a remove for each now. 6544 for (nic in remove) { 6545 nic = remove[nic]; 6546 zcfg = zcfg + 'remove net mac-addr=' + ruinMac(nic) + '\n'; 6547 } 6548 6549 // properties that don't require any validation - add them if they're 6550 // present: 6551 var nicProperties = ['ip', 'netmask', 'network_uuid', 'model', 6552 'dhcp_server', 'allow_dhcp_spoofing', 'blocked_outgoing_ports', 6553 'allow_ip_spoofing', 'allow_mac_spoofing', 'allow_restricted_traffic', 6554 'allow_unfiltered_promisc', 'vrrp_vrid', 'vrrp_primary_ip']; 6555 6556 for (nic in add) { 6557 nic = add[nic]; 6558 6559 zcfg = zcfg 6560 + 'add net\n' 6561 + 'set physical=' + nic.interface + '\n' 6562 + 'set mac-addr=' + ruinMac(nic.mac) + '\n'; 6563 6564 if (nic.hasOwnProperty('nic_tag')) { 6565 zcfg = zcfg + 'set global-nic=' + nic.nic_tag + '\n'; 6566 } 6567 6568 if (nic.hasOwnProperty('gateway') && nic.gateway.length > 0) { 6569 zcfg = zcfg + 'add property (name=gateway, value="' 6570 + nic.gateway + '")\n'; 6571 } 6572 6573 if (nic.hasOwnProperty('primary') && nic.primary) { 6574 zcfg = zcfg + 'add property (name=primary, value="true")\n'; 6575 } 6576 6577 if (nic.hasOwnProperty('vlan_id') && (nic.vlan_id !== '0')) { 6578 zcfg = zcfg + 'set vlan-id=' + nic.vlan_id + '\n'; 6579 } 6580 6581 if (nic.hasOwnProperty('allowed_ips')) { 6582 zcfg = zcfg 6583 + 'add property (name=allowed_ips, value="' 6584 + nic.allowed_ips.join(',') + '")\n'; 6585 } 6586 6587 for (var prop in nicProperties) { 6588 prop = nicProperties[prop]; 6589 if (nic.hasOwnProperty(prop)) { 6590 zcfg = zcfg + 'add property (name=' + prop + ', value="' 6591 + nic[prop] + '")\n'; 6592 } 6593 } 6594 6595 zcfg = zcfg + 'end\n'; 6596 } 6597 6598 return zcfg; 6599 } 6600 6601 function buildFilesystemZonecfg(vmobj, payload) 6602 { 6603 var add = []; 6604 var filesystem; 6605 var lists; 6606 var opt; 6607 var remove = []; 6608 var zcfg = ''; 6609 6610 lists = buildAddRemoveList(vmobj, payload, 'filesystem', 'target', []); 6611 remove = lists.remove; 6612 add = lists.add; 6613 6614 // remove is a list of disk paths, add a remove for each now. 6615 for (filesystem in remove) { 6616 filesystem = remove[filesystem]; 6617 zcfg = zcfg + 'remove fs match=' + filesystem + '\n'; 6618 } 6619 6620 for (filesystem in add) { 6621 filesystem = add[filesystem]; 6622 6623 zcfg = zcfg + 'add fs\n' + 'set dir=' + filesystem.target + '\n' 6624 + 'set special=' + filesystem.source + '\n' + 'set type=' 6625 + filesystem.type + '\n'; 6626 if (filesystem.hasOwnProperty('raw')) { 6627 zcfg = zcfg + 'set raw=' + filesystem.raw + '\n'; 6628 } 6629 if (filesystem.hasOwnProperty('options')) { 6630 for (opt in filesystem.options) { 6631 opt = filesystem.options[opt]; 6632 zcfg = zcfg + 'add options "' + opt + '"\n'; 6633 } 6634 } 6635 zcfg = zcfg + 'end\n'; 6636 } 6637 6638 return zcfg; 6639 } 6640 6641 function buildZonecfgUpdate(vmobj, payload, log) 6642 { 6643 var brand; 6644 var tmp; 6645 var zcfg = ''; 6646 6647 assert(log, 'no logger passed to buildZonecfgUpdate()'); 6648 6649 log.debug({vmobj: vmobj, payload: payload}, 6650 'parameters to buildZonecfgUpdate()'); 6651 6652 if (vmobj && vmobj.hasOwnProperty('brand')) { 6653 brand = vmobj.brand; 6654 } else { 6655 brand = payload.brand; 6656 } 6657 6658 // Global properties can just be set, no need to clear anything first. 6659 if (payload.hasOwnProperty('cpu_shares')) { 6660 zcfg = zcfg + 'set cpu-shares=' + payload.cpu_shares.toString() + '\n'; 6661 } 6662 if (payload.hasOwnProperty('zfs_io_priority')) { 6663 zcfg = zcfg + 'set zfs-io-priority=' 6664 + payload.zfs_io_priority.toString() + '\n'; 6665 } 6666 if (payload.hasOwnProperty('max_lwps')) { 6667 zcfg = zcfg + 'set max-lwps=' + payload.max_lwps.toString() + '\n'; 6668 } 6669 if (payload.hasOwnProperty('limit_priv')) { 6670 zcfg = zcfg + 'set limitpriv="' + payload.limit_priv + '"\n'; 6671 } 6672 6673 if (!BRAND_OPTIONS[brand].features.use_vm_autoboot 6674 && payload.hasOwnProperty('autoboot')) { 6675 6676 // kvm autoboot is managed by the vm-autoboot attr instead 6677 zcfg = zcfg + 'set autoboot=' + payload.autoboot.toString() + '\n'; 6678 } 6679 6680 // Capped Memory properties are special 6681 if (payload.hasOwnProperty('max_physical_memory') 6682 || payload.hasOwnProperty('max_locked_memory') 6683 || payload.hasOwnProperty('max_swap')) { 6684 6685 // Capped memory parameters need either an add or select first. 6686 if (vmobj.hasOwnProperty('max_physical_memory') 6687 || vmobj.hasOwnProperty('max_locked_memory') 6688 || vmobj.hasOwnProperty('max_swap')) { 6689 6690 // there's already a capped-memory section, use that. 6691 zcfg = zcfg + 'select capped-memory; '; 6692 } else { 6693 zcfg = zcfg + 'add capped-memory; '; 6694 } 6695 6696 if (payload.hasOwnProperty('max_physical_memory')) { 6697 zcfg = zcfg + 'set physical=' 6698 + payload.max_physical_memory.toString() + 'm; '; 6699 } 6700 if (payload.hasOwnProperty('max_locked_memory')) { 6701 zcfg = zcfg + 'set locked=' 6702 + payload.max_locked_memory.toString() + 'm; '; 6703 } 6704 if (payload.hasOwnProperty('max_swap')) { 6705 zcfg = zcfg + 'set swap=' 6706 + payload.max_swap.toString() + 'm; '; 6707 } 6708 6709 zcfg = zcfg + 'end\n'; 6710 } 6711 6712 // Capped CPU is special 6713 if (payload.hasOwnProperty('cpu_cap')) { 6714 if (vmobj.hasOwnProperty('cpu_cap')) { 6715 zcfg = zcfg + 'select capped-cpu; '; 6716 } else { 6717 zcfg = zcfg + 'add capped-cpu; '; 6718 } 6719 6720 zcfg = zcfg + 'set ncpus=' 6721 + (Number(payload.cpu_cap) * 0.01).toString() + '; end\n'; 6722 } 6723 6724 // set to empty string so property is removed when not true or when not 6725 // false if that's the default for the property. 6726 if (payload.hasOwnProperty('do_not_inventory')) { 6727 if (payload.do_not_inventory !== true) { 6728 // removing sets false as that's the default. 6729 payload.do_not_inventory = ''; 6730 } 6731 } 6732 6733 if (payload.hasOwnProperty('archive_on_delete')) { 6734 if (payload.archive_on_delete !== true) { 6735 // removing sets false as that's the default. 6736 payload.archive_on_delete = ''; 6737 } 6738 } 6739 6740 if (payload.hasOwnProperty('firewall_enabled')) { 6741 if (payload.firewall_enabled !== true) { 6742 // removing sets false as that's the default. 6743 payload.firewall_enabled = ''; 6744 } 6745 } 6746 6747 if (payload.hasOwnProperty('restart_init')) { 6748 if (payload.restart_init === true) { 6749 // removing sets true as that's the default. 6750 payload.restart_init = ''; 6751 } 6752 } 6753 6754 // Attributes 6755 function setAttr(attr, attr_name, value) { 6756 if (!value) { 6757 value = payload[attr_name]; 6758 } 6759 6760 if (payload.hasOwnProperty(attr_name)) { 6761 if ((typeof (value) !== 'boolean') 6762 && (!value || trim(value.toString()) === '')) { 6763 6764 // empty values we either remove or ignore. 6765 if (vmobj.hasOwnProperty(attr_name)) { 6766 zcfg = zcfg + 'remove attr name=' + attr + ';'; 6767 // else do nothing, we don't add empty values. 6768 } 6769 } else { 6770 if (attr_name === 'resolvers' 6771 && vmobj.hasOwnProperty('resolvers') 6772 && vmobj.resolvers.length === 0) { 6773 6774 // special case for resolvers: we always have 'resolvers' 6775 // in the object, but if it's empty we don't have it in the 6776 // zonecfg. Add instead of the usual update. 6777 zcfg = zcfg + 'add attr; set name="' + attr + '"; ' 6778 + 'set type=string; '; 6779 } else if (vmobj.hasOwnProperty(attr_name)) { 6780 zcfg = zcfg + 'select attr name=' + attr + '; '; 6781 } else { 6782 zcfg = zcfg + 'add attr; set name="' + attr + '"; ' 6783 + 'set type=string; '; 6784 } 6785 zcfg = zcfg + 'set value="' + value.toString() + '"; end\n'; 6786 } 6787 } 6788 } 6789 setAttr('billing-id', 'billing_id'); 6790 setAttr('owner-uuid', 'owner_uuid'); 6791 setAttr('package-name', 'package_name'); 6792 setAttr('package-version', 'package_version'); 6793 setAttr('tmpfs', 'tmpfs'); 6794 setAttr('hostname', 'hostname'); 6795 setAttr('dns-domain', 'dns_domain'); 6796 setAttr('default-gateway', 'default_gateway'); 6797 setAttr('do-not-inventory', 'do_not_inventory'); 6798 setAttr('archive-on-delete', 'archive_on_delete'); 6799 setAttr('firewall-enabled', 'firewall_enabled'); 6800 setAttr('restart-init', 'restart_init'); 6801 setAttr('init-name', 'init_name'); 6802 setAttr('disk-driver', 'disk_driver'); 6803 setAttr('nic-driver', 'nic_driver'); 6804 6805 if (payload.hasOwnProperty('resolvers')) { 6806 setAttr('resolvers', 'resolvers', payload.resolvers.join(',')); 6807 } 6808 if (payload.hasOwnProperty('alias')) { 6809 tmp = ''; 6810 if (payload.alias) { 6811 tmp = new Buffer(payload.alias).toString('base64'); 6812 } 6813 setAttr('alias', 'alias', tmp); 6814 } 6815 6816 if (BRAND_OPTIONS[brand].features.use_vm_autoboot) { 6817 setAttr('vm-autoboot', 'autoboot'); 6818 } 6819 6820 // XXX Used on KVM but can be passed in for 'OS' too. We only setAttr on KVM 6821 if (BRAND_OPTIONS[brand].features.type === 'KVM') { 6822 setAttr('ram', 'ram'); 6823 } 6824 6825 // NOTE: Thanks to normalizePayload() we'll only have these when relevant 6826 setAttr('vcpus', 'vcpus'); 6827 setAttr('boot', 'boot'); 6828 setAttr('cpu-type', 'cpu_type'); 6829 setAttr('vga', 'vga'); 6830 setAttr('vnc-port', 'vnc_port'); 6831 setAttr('spice-port', 'spice_port'); 6832 setAttr('virtio-txtimer', 'virtio_txtimer'); 6833 setAttr('virtio-txburst', 'virtio_txburst'); 6834 6835 // We use base64 here for these next five options: 6836 // 6837 // vnc_password 6838 // spice_password 6839 // spice_opts 6840 // qemu_opts 6841 // qemu_extra_opts 6842 // 6843 // since these can contain characters zonecfg doesn't like. 6844 // 6845 if (payload.hasOwnProperty('vnc_password')) { 6846 if (payload.vnc_password === '' 6847 && (vmobj.hasOwnProperty('vnc_password') 6848 && vmobj.vnc_password !== '')) { 6849 6850 log.warn('Warning: VNC password was removed for VM ' 6851 + vmobj.uuid + ' but VM needs to be restarted for change to' 6852 + 'take effect.'); 6853 } 6854 if (payload.vnc_password.length > 0 6855 && !vmobj.hasOwnProperty('vnc_password')) { 6856 6857 log.warn('Warning: VNC password was added to VM ' 6858 + vmobj.uuid + ' but VM needs to be restarted for change to' 6859 + 'take effect.'); 6860 } 6861 6862 setAttr('vnc-password', 'vnc_password', 6863 new Buffer(payload.vnc_password).toString('base64')); 6864 } 6865 if (payload.hasOwnProperty('spice_password')) { 6866 if (payload.spice_password === '' 6867 && (vmobj.hasOwnProperty('spice_password') 6868 && vmobj.spice_password !== '')) { 6869 6870 log.warn('Warning: SPICE password was removed for VM ' 6871 + vmobj.uuid + ' but VM needs to be restarted for change to' 6872 + 'take effect.'); 6873 } 6874 if (payload.spice_password.length > 0 6875 && !vmobj.hasOwnProperty('spice_password')) { 6876 6877 log.warn('Warning: SPICE password was added to VM ' 6878 + vmobj.uuid + ' but VM needs to be restarted for change to' 6879 + 'take effect.'); 6880 } 6881 6882 setAttr('spice-password', 'spice_password', 6883 new Buffer(payload.spice_password).toString('base64')); 6884 } 6885 if (payload.hasOwnProperty('spice_opts')) { 6886 setAttr('spice-opts', 'spice_opts', 6887 new Buffer(payload.spice_opts).toString('base64')); 6888 } 6889 if (payload.hasOwnProperty('qemu_opts')) { 6890 setAttr('qemu-opts', 'qemu_opts', 6891 new Buffer(payload.qemu_opts).toString('base64')); 6892 } 6893 if (payload.hasOwnProperty('qemu_extra_opts')) { 6894 setAttr('qemu-extra-opts', 'qemu_extra_opts', 6895 new Buffer(payload.qemu_extra_opts).toString('base64')); 6896 } 6897 6898 // Handle disks 6899 if (payload.hasOwnProperty('disks') 6900 || payload.hasOwnProperty('add_disks') 6901 || payload.hasOwnProperty('update_disks') 6902 || payload.hasOwnProperty('remove_disks')) { 6903 6904 zcfg = zcfg + buildDiskZonecfg(vmobj, payload); 6905 } 6906 6907 if (payload.hasOwnProperty('fs_allowed')) { 6908 if (payload.fs_allowed === '') { 6909 zcfg = zcfg + 'clear fs-allowed\n'; 6910 } else { 6911 zcfg = zcfg + 'set fs-allowed="' + payload.fs_allowed + '"\n'; 6912 } 6913 } 6914 6915 if (payload.hasOwnProperty('filesystems') 6916 || payload.hasOwnProperty('add_filesystems') 6917 || payload.hasOwnProperty('update_filesystems') 6918 || payload.hasOwnProperty('add_filesystems')) { 6919 6920 zcfg = zcfg + buildFilesystemZonecfg(vmobj, payload); 6921 } 6922 6923 zcfg = zcfg + buildNicZonecfg(vmobj, payload); 6924 6925 return zcfg; 6926 } 6927 6928 // Checks that QMP is responding to query-status and if so passes the boolean 6929 // value of the hwsetup parameter to the callback. 6930 // 6931 // vmobj must have: 6932 // 6933 // zonepath 6934 // 6935 function checkHWSetup(vmobj, log, callback) 6936 { 6937 var q; 6938 var socket; 6939 6940 assert(log, 'no logger passed to checkHWSetup()'); 6941 6942 q = new Qmp(log); 6943 socket = vmobj.zonepath + '/root/tmp/vm.qmp'; 6944 6945 q.connect(socket, function (error) { 6946 if (error) { 6947 log.error(error, 'q.connect(): Error: ' + error.message); 6948 callback(error); 6949 return; 6950 } 6951 q.command('query-status', null, function (e, result) { 6952 if (e) { 6953 log.error(e, 'q.command(query-status): Error: ' + e.message); 6954 callback(e); 6955 return; 6956 } 6957 q.disconnect(); 6958 callback(null, result.hwsetup ? true : false); 6959 return; 6960 }); 6961 }); 6962 } 6963 6964 // cb (if set) will be called with an Error if we can't setup the interval loop 6965 // otherwise when the loop is shut down. 6966 // 6967 // vmobj must have: 6968 // 6969 // brand 6970 // state 6971 // uuid 6972 // zonepath 6973 // zoneroot 6974 // 6975 function markProvisionedWhenHWSetup(vmobj, options, cb) 6976 { 6977 var ival_handle; 6978 var log; 6979 var loop_interval = 3; // seconds 6980 var zoneroot; 6981 6982 log = options.log; 6983 assert(log, 'no logger passed to markProvisionedWenHWSetup()'); 6984 assert(vmobj.hasOwnProperty('zonepath'), 'no zonepath in vmobj'); 6985 6986 zoneroot = path.join(vmobj.zoneroot, '/root'); 6987 6988 if (!BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) { 6989 // do nothing for zones where we don't wait for hwsetup 6990 cb(new Error('brand ' + vmobj.brand + ' does not support hwsetup')); 6991 return (null); 6992 } 6993 6994 // Ensure the dataset doesn't have unsafe links as /var or /var/svc 6995 // Since we're checking the 'file' provision_success, this also guarantees 6996 // that if it already exists, it's not a symlink. 6997 try { 6998 assertSafeZonePath(zoneroot, '/var/svc/provision_success', 6999 {type: 'file', enoent_ok: true}); 7000 } catch (e) { 7001 cb(e); 7002 return (null); 7003 } 7004 7005 if (!options) { 7006 options = {}; 7007 } 7008 7009 // if caller wants they can change the interval 7010 if (options.hasOwnProperty('interval')) { 7011 loop_interval = options.interval; 7012 } 7013 7014 log.debug('setting hwsetup interval ' + vmobj.uuid); 7015 ival_handle = setInterval(function () { 7016 VM.load(vmobj.uuid, {fields: ['transition_expire', 'uuid'], log: log}, 7017 function (err, obj) { 7018 7019 var timeout_remaining; 7020 var ival = ival_handle; 7021 7022 function done() { 7023 if (ival_handle) { 7024 log.debug('clearing hwsetup interval ' + vmobj.uuid); 7025 clearInterval(ival); 7026 ival = null; 7027 } else { 7028 log.debug('done but no hwsetup interval ' + vmobj.uuid); 7029 } 7030 } 7031 7032 if (err) { 7033 // If the VM was deleted between calls, nothing much we can do. 7034 log.error(err, 'Unable to load ' + vmobj.uuid + ' ' 7035 + err.message); 7036 done(); 7037 cb(err); 7038 return; 7039 } 7040 7041 // we only do anything if we're still waiting for provisioning 7042 if (vmobj.state !== 'provisioning') { 7043 done(); 7044 cb(); 7045 return; 7046 } 7047 7048 timeout_remaining = 7049 (Number(obj.transition_expire) - Date.now(0)) / 1000; 7050 7051 if (timeout_remaining <= 0) { 7052 // IMPORTANT: this may run multiple times, must be idempotent 7053 7054 log.warn('Marking VM ' + vmobj.uuid + ' as "failed" because' 7055 + ' timeout expired and we are still "provisioning"'); 7056 VM.markVMFailure(vmobj, {log: log}, function (mark_err) { 7057 log.warn(mark_err, 'zoneinit failed, zone is ' 7058 + 'being stopped for manual investigation.'); 7059 done(); 7060 cb(); 7061 }); 7062 return; 7063 } 7064 7065 checkHWSetup(vmobj, log, function (check_err, result) { 7066 if (check_err) { 7067 log.debug(check_err, 'checkHWSetup Error: ' 7068 + check_err.message); 7069 return; 7070 } 7071 7072 if (result) { 7073 log.debug('QMP says VM ' + vmobj.uuid 7074 + ' completed hwsetup'); 7075 VM.unsetTransition(vmobj, {log: log}, function (unset_err) { 7076 var provisioning; 7077 var provision_success; 7078 7079 provisioning = path.join(vmobj.zonepath, 7080 '/root/var/svc/provisioning'); 7081 provision_success = path.join(vmobj.zonepath, 7082 '/root/var/svc/provision_success'); 7083 7084 if (unset_err) { 7085 log.error(unset_err); 7086 } else { 7087 log.debug('cleared transition to provisioning on' 7088 + ' ' + vmobj.uuid); 7089 } 7090 7091 fs.rename(provisioning, provision_success, 7092 function (e) { 7093 7094 if (e) { 7095 if (e.code === 'ENOENT') { 7096 log.debug(e); 7097 } else { 7098 log.error(e); 7099 } 7100 } 7101 7102 done(); 7103 cb(); 7104 return; 7105 }); 7106 }); 7107 } 7108 }); 7109 }); 7110 }, loop_interval * 1000); 7111 7112 return (ival_handle); 7113 } 7114 7115 function archiveVM(uuid, options, callback) 7116 { 7117 var archive_dirname; 7118 var dirmode; 7119 var log; 7120 var patterns_to_archive = []; 7121 var vmobj; 7122 7123 /*jsl:ignore*/ 7124 dirmode = 0755; 7125 /*jsl:end*/ 7126 7127 if (options.hasOwnProperty('log')) { 7128 log = options.log; 7129 } else { 7130 log = VM.log; 7131 } 7132 7133 log.debug('attempting to archive debug data for VM ' + uuid); 7134 7135 async.series([ 7136 function (cb) { 7137 // ensure directory exists 7138 archive_dirname = path.join('/zones/archive', uuid); 7139 7140 fs.mkdir(archive_dirname, dirmode, function (e) { 7141 log.debug(e, 'attempted to create ' + archive_dirname); 7142 cb(e); 7143 return; 7144 }); 7145 }, function (cb) { 7146 VM.load(uuid, {log: log}, function (err, obj) { 7147 if (err) { 7148 cb(err); 7149 return; 7150 } 7151 vmobj = obj; 7152 cb(); 7153 }); 7154 }, function (cb) { 7155 // write vmobj to archive 7156 var filename; 7157 7158 filename = path.join(archive_dirname, 'vm.json'); 7159 7160 fs.writeFile(filename, JSON.stringify(vmobj, null, 2) + '\n', 7161 function (err, result) { 7162 7163 if (err) { 7164 log.error(err, 'failed to create ' + filename + ': ' 7165 + err.message); 7166 } else { 7167 log.info('archived data to ' + filename); 7168 } 7169 7170 cb(); // ignore error 7171 }); 7172 }, function (cb) { 7173 var cmdline = '/usr/sbin/zfs list -t all -o name | grep ' 7174 + vmobj.zonename + ' | xargs zfs get -pH all >' 7175 + path.join(archive_dirname, 'zfs.dump'); 7176 7177 log.debug(cmdline); 7178 exec(cmdline, function (e, stdout, stderr) { 7179 if (e) { 7180 e.stdout = stdout; 7181 e.stderr = stderr; 7182 log.error({err: e}, 'failed to create ' 7183 + path.join(archive_dirname, 'zfs.dump')); 7184 cb(e); 7185 return; 7186 } 7187 log.info('archived data to ' + path.join(archive_dirname, 7188 'zfs.dump')); 7189 cb(); 7190 }); 7191 }, function (cb) { 7192 patterns_to_archive.push({ 7193 src: path.join('/etc/zones/', vmobj.zonename + '.xml'), 7194 dst: path.join(archive_dirname, 'zone.xml') 7195 }); 7196 patterns_to_archive.push({ 7197 src: path.join(vmobj.zonepath, 'config'), 7198 dst: archive_dirname, 7199 targ: path.join(archive_dirname, 'config') 7200 }); 7201 patterns_to_archive.push({ 7202 src: path.join(vmobj.zonepath, 'cores'), 7203 dst: archive_dirname, 7204 targ: path.join(archive_dirname, 'cores') 7205 }); 7206 7207 if (vmobj.brand === 'kvm') { 7208 patterns_to_archive.push({ 7209 src: path.join(vmobj.zonepath, 'root/tmp/vm*.log*'), 7210 dst: path.join(archive_dirname, 'vmlogs'), 7211 create_dst_dir: true 7212 }); 7213 patterns_to_archive.push({ 7214 src: path.join(vmobj.zonepath, 'root/startvm'), 7215 dst: archive_dirname, 7216 targ: path.join(archive_dirname, 'startvm') 7217 }); 7218 } else { 7219 patterns_to_archive.push({ 7220 src: path.join(vmobj.zonepath, 'root/var/svc/log/*'), 7221 dst: path.join(archive_dirname, 'svclogs'), 7222 create_dst_dir: true 7223 }); 7224 patterns_to_archive.push({ 7225 src: path.join(vmobj.zonepath, 'root/var/adm/messages*'), 7226 dst: path.join(archive_dirname, 'admmsgs'), 7227 create_dst_dir: true 7228 }); 7229 } 7230 7231 async.forEachSeries(patterns_to_archive, function (pattern, c) { 7232 7233 function cpPattern(p, cp_cb) { 7234 var cmdline = '/usr/bin/cp -RP ' + p.src + ' ' + p.dst; 7235 var targ = p.targ || p.dst; 7236 7237 log.debug(cmdline); 7238 exec(cmdline, function (e, stdout, stderr) { 7239 if (e) { 7240 e.stdout = stdout; 7241 e.stderr = stderr; 7242 log.error({err: e}, 'failed to archive data to ' 7243 + targ); 7244 } else { 7245 log.info('archived data to ' + targ); 7246 } 7247 // we don't return errors here because on error copying 7248 // one pattern we still want to grab the others. 7249 cp_cb(); 7250 }); 7251 } 7252 7253 if (pattern.create_dst_dir) { 7254 fs.mkdir(pattern.dst, dirmode, function (e) { 7255 if (!e) { 7256 log.info('created ' + pattern.dst); 7257 } else { 7258 log.error({err: e}, 'failed to create ' 7259 + pattern.dst); 7260 } 7261 cpPattern(pattern, c); 7262 }); 7263 } else { 7264 cpPattern(pattern, c); 7265 } 7266 }, function (e) { 7267 log.info('finished archiving VM ' + vmobj.uuid); 7268 cb(e); 7269 }); 7270 } 7271 ], function () { 7272 // XXX we ignore errors as failures to archive will not block VM delete. 7273 callback(); 7274 }); 7275 } 7276 7277 // vmobj argument should have: 7278 // 7279 // transition_to 7280 // uuid 7281 // zonename 7282 // 7283 exports.markVMFailure = function (vmobj, options, cb) 7284 { 7285 var log; 7286 7287 // options is optional 7288 if (arguments.length === 2) { 7289 cb = arguments[1]; 7290 options = {}; 7291 } 7292 7293 if (!vmobj || !vmobj.hasOwnProperty('uuid') 7294 || !vmobj.hasOwnProperty('zonename')) { 7295 7296 cb(new Error('markVMFailure needs uuid + zonename')); 7297 return; 7298 } 7299 7300 ensureLogging(true); 7301 if (options.hasOwnProperty('log')) { 7302 log = options.log; 7303 } else { 7304 log = VM.log.child({action: 'markVMFailure', vm: vmobj.uuid}); 7305 } 7306 7307 function dumpDebugInfo(zonename, callback) { 7308 var errors = {}; 7309 7310 async.series([ 7311 function (ptree_cb) { 7312 // note: if the zone is not running this returns empty but still 7313 // exits 0 7314 execFile('/usr/bin/ptree', ['-z', zonename], 7315 function (ptree_err, ptree_stdout, ptree_stderr) { 7316 7317 if (ptree_err) { 7318 log.error(ptree_err, 'unable to get ptree from ' 7319 + zonename + ': ' + ptree_stderr); 7320 errors.ptree_err = ptree_err; 7321 } else { 7322 log.warn('processes running in ' + zonename 7323 + ' at fail time:\n' + ptree_stdout); 7324 } 7325 7326 ptree_cb(); // don't fail on error here. 7327 } 7328 ); 7329 }, function (svcs_cb) { 7330 execFile('/usr/bin/svcs', ['-xv', '-z', zonename], 7331 function (svcs_err, svcs_stdout, svcs_stderr) { 7332 7333 if (svcs_err) { 7334 log.error(svcs_err, 'unable to get svcs from ' 7335 + zonename + ': ' + svcs_stderr); 7336 errors.svcs_err = svcs_err; 7337 } else { 7338 log.warn('svcs -xv output for ' + zonename 7339 + ' at fail time:\n' + svcs_stdout); 7340 } 7341 7342 svcs_cb(); // don't fail on error here. 7343 } 7344 ); 7345 }, function (kstat_cb) { 7346 execFile('/usr/bin/kstat', ['-n', zonename.substr(0, 30)], 7347 function (kstat_err, kstat_stdout, kstat_stderr) { 7348 7349 if (kstat_err) { 7350 log.error(kstat_err, 'unable to get kstats from ' 7351 + zonename + ': ' + kstat_stderr); 7352 errors.kstat_err = kstat_err; 7353 } else { 7354 log.warn('kstat output for ' + zonename 7355 + ' at fail time:\n' + kstat_stdout); 7356 } 7357 7358 kstat_cb(); // don't fail on error here. 7359 } 7360 ); 7361 } 7362 ], function () { 7363 callback(errors); 7364 }); 7365 } 7366 7367 dumpDebugInfo(vmobj.zonename, function (debug_err) { 7368 var zcfg; 7369 7370 // note: we don't treat failure to dump debug info as a fatal error. 7371 log.warn(debug_err, 'zone setup failed, zone is being stopped ' 7372 + 'for manual investigation.'); 7373 7374 // Mark the zone as 'failed' 7375 zcfg = 'remove -F attr name=failed; add attr; set name=failed; ' 7376 + 'set value="provisioning"; set type=string; end'; 7377 7378 zonecfg(['-u', vmobj.uuid, zcfg], log, function (zonecfg_err, fds) { 7379 7380 if (zonecfg_err) { 7381 log.error({err: zonecfg_err, stdout: fds.stdout, 7382 stderr: fds.stderr}, 'Unable to set failure flag on ' 7383 + vmobj.uuid + ': ' + zonecfg_err.message); 7384 } else { 7385 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 7386 'set failure flag on ' + vmobj.uuid); 7387 } 7388 7389 // attempt to remove transition 7390 VM.unsetTransition(vmobj, {log: log}, function (unset_err) { 7391 if (unset_err) { 7392 log.error(unset_err); 7393 } 7394 7395 VM.stop(vmobj.uuid, {force: true, log: log}, 7396 function (stop_err) { 7397 7398 // only log errors because there's nothing to do 7399 7400 if (stop_err) { 7401 log.error(stop_err, 'failed to stop VM ' 7402 + vmobj.uuid + ': ' + stop_err.message); 7403 } 7404 7405 cb(); 7406 }); 7407 }); 7408 }); 7409 }); 7410 }; 7411 7412 function svccfg(zonepath, args, log, callback) 7413 { 7414 var cmd = '/usr/sbin/svccfg'; 7415 var exec_options = {}; 7416 var zoneroot = path.join(zonepath, '/root'); 7417 7418 assert(log, 'no logger passed to svccfg()'); 7419 7420 try { 7421 assertSafeZonePath(zoneroot, '/etc/svc/repository.db', 7422 {type: 'file', enoent_ok: false}); 7423 } catch (e) { 7424 log.error(e, 'Error validating /etc/svc/repository.db: ' + e.message); 7425 callback(e); 7426 return; 7427 } 7428 7429 exec_options = { 7430 env: { 7431 'SVCCFG_CONFIGD_PATH': '/lib/svc/bin/svc.configd', 7432 'SVCCFG_REPOSITORY': 7433 path.join(zonepath, 'root', '/etc/svc/repository.db') 7434 } 7435 }; 7436 7437 log.debug({'command': cmd + ' ' + args.join(' '), 7438 'exec_options': exec_options}, 'modifying svc repo in ' + zonepath); 7439 execFile(cmd, args, exec_options, function (error, stdout, stderr) { 7440 if (error) { 7441 callback(error, {'stdout': stdout, 'stderr': stderr}); 7442 } else { 7443 callback(null, {'stdout': stdout, 'stderr': stderr}); 7444 } 7445 }); 7446 } 7447 7448 // This calls cb() when /var/svc/provisioning is gone. When this calls cb() 7449 // with an Error object, the provision is considered failed so this should 7450 // only happen when something timed out that is unrelated to the user. 7451 // 7452 // This returns a function that can be called with no arguments to cancel 7453 // all timers and actions pending from this function. It will also then not 7454 // call the cb(). 7455 // 7456 // IMPORTANT: this is only exported to be used by vmadmd. Do not use elsewhere! 7457 // 7458 // vmobj fields: 7459 // 7460 // state 7461 // transition_expire 7462 // uuid 7463 // zonepath 7464 // 7465 exports.waitForProvisioning = function (vmobj, options, cb) 7466 { 7467 var dirname = path.join(vmobj.zonepath, 'root', '/var/svc'); 7468 var filename = path.join(dirname, 'provisioning'); 7469 var ival_h; 7470 var log; 7471 var timeout; 7472 var timeout_remaining = PROVISION_TIMEOUT; // default to whole thing 7473 var watcher; 7474 7475 // options is optional 7476 if (arguments.length === 2) { 7477 cb = arguments[1]; 7478 options = {}; 7479 } 7480 7481 ensureLogging(true); 7482 if (options.hasOwnProperty('log')) { 7483 log = options.log; 7484 } else { 7485 log = VM.log.child({action: 'waitForProvisioning', vm: vmobj.uuid}); 7486 } 7487 7488 function done() { 7489 if (timeout) { 7490 log.debug('clearing provision timeout for ' + vmobj.uuid); 7491 clearTimeout(timeout); 7492 timeout = null; 7493 } 7494 if (watcher) { 7495 log.debug('closing /var/svc/provisioning watcher for ' 7496 + vmobj.uuid); 7497 watcher.close(); 7498 watcher = null; 7499 } 7500 if (ival_h) { 7501 log.debug('closing hwsetup check interval for ' + vmobj.uuid); 7502 clearInterval(ival_h); 7503 ival_h = null; 7504 } 7505 } 7506 7507 if ((vmobj.state === 'provisioning') 7508 && (vmobj.hasOwnProperty('transition_expire'))) { 7509 7510 timeout_remaining = 7511 (Number(vmobj.transition_expire) - Date.now(0)) / 1000; 7512 7513 // Always give it at least 1 second's chance. 7514 if (timeout_remaining < 1) { 7515 timeout_remaining = 1; 7516 } 7517 } else { 7518 // don't know what to do here we're not provisioning. 7519 log.warn('waitForProvisioning called when ' + vmobj.uuid 7520 + ' was not provisioning'); 7521 cb(); 7522 return (null); 7523 } 7524 7525 log.debug({ 7526 'transition_expire': Number(vmobj.transition_expire), 7527 'now': Date.now(0) 7528 }, 'waiting ' + timeout_remaining + ' sec(s) for provisioning'); 7529 7530 log.debug('setting provision timeout for ' + vmobj.uuid); 7531 timeout = setTimeout(function () { 7532 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure" because we ' 7533 + 'hit waitForProvisioning() timeout.'); 7534 VM.markVMFailure(vmobj, {log: log}, function (err) { 7535 var errstr = 'timed out waiting for /var/svc/provisioning to move' 7536 + ' for ' + vmobj.uuid; 7537 if (err) { 7538 log.warn(err, 'markVMFailure(): ' + err.message); 7539 } 7540 log.error(errstr); 7541 done(); 7542 cb(new Error(errstr)); 7543 }); 7544 }, (timeout_remaining * 1000)); 7545 7546 // this starts a loop that will move provisioning -> provision_success when 7547 // the hardware of the VM has been initialized the first time. 7548 if (BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) { 7549 ival_h = markProvisionedWhenHWSetup(vmobj, {log: log}, function (err) { 7550 if (err) { 7551 log.error(err, 'error in markProvisionedWhenHWSetup()'); 7552 } 7553 done(); 7554 cb(err); 7555 }); 7556 return (done); 7557 } 7558 7559 watcher = fs.watch(filename, function (evt, file) { 7560 // We only care about 'rename' which also fires when the file is 7561 // deleted. 7562 log.debug('watcher.event(' + vmobj.uuid + '): ' + evt); 7563 if (evt === 'rename') { 7564 fs.exists(filename, function (exists) { 7565 if (exists) { 7566 // somehow we still have /var/svc/provisioning! 7567 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure"' 7568 + ' because we still have /var/svc/provisioning after ' 7569 + 'rename'); 7570 VM.markVMFailure(vmobj, {log: log}, function (err) { 7571 if (err) { 7572 log.warn(err, 'markVMFailure(): ' + err.message); 7573 } 7574 done(); 7575 cb(new Error('/var/svc/provisioning exists after ' 7576 + 'rename!')); 7577 }); 7578 return; 7579 } 7580 7581 // So long as /var/svc/provisioning is gone, we don't care what 7582 // replaced it. Success or failure of user script doesn't 7583 // matter for the state, it's provisioned now. Caller should 7584 // now clear the transition. 7585 done(); 7586 cb(); 7587 return; 7588 }); 7589 } 7590 }); 7591 7592 log.debug('created watcher for ' + vmobj.uuid); 7593 return (done); 7594 }; 7595 7596 // create and install a 'joyent' or 'kvm' brand zone. 7597 function installZone(payload, log, callback) 7598 { 7599 var load_fields; 7600 var receiving = false; 7601 var reprovisioning = false; 7602 var vmobj; 7603 var zoneinit = {}; 7604 7605 assert(log, 'no logger passed to installZone()'); 7606 7607 log.debug('installZone()'); 7608 7609 load_fields = [ 7610 'brand', 7611 'firewall_enabled', 7612 'missing', 7613 'nics', 7614 'owner_uuid', 7615 'routes', 7616 'state', 7617 'tags', 7618 'transition_to', 7619 'transition_expire', 7620 'uuid', 7621 'zonename', 7622 'zonepath' 7623 ]; 7624 7625 if (payload.reprovisioning) { 7626 log.debug('installZone(): reprovisioning'); 7627 reprovisioning = true; 7628 } 7629 7630 async.series([ 7631 function (cb) { 7632 7633 VM.load(payload.uuid, {fields: load_fields, log: log}, 7634 function (err, obj) { 7635 7636 if (err) { 7637 cb(err); 7638 return; 7639 } 7640 vmobj = obj; 7641 cb(); 7642 }); 7643 }, function (cb) { 7644 var thing; 7645 var missing = false; 7646 var msg; 7647 var things = ['datasets', 'filesystems', 'disks']; 7648 7649 if (vmobj.state === 'receiving') { 7650 receiving = true; 7651 msg = 'zone is still missing:'; 7652 for (thing in things) { 7653 thing = things[thing]; 7654 if (vmobj.missing[thing].length !== 0) { 7655 msg = msg + ' ' + vmobj.missing[thing].length + ' ' 7656 + thing + ','; 7657 missing = true; 7658 } 7659 } 7660 msg = rtrim(msg, ','); 7661 7662 if (missing) { 7663 cb(new Error('Unable to complete install for ' 7664 + vmobj.uuid + ' ' + msg)); 7665 return; 7666 } 7667 } 7668 cb(); 7669 }, function (cb) { 7670 // Install the zone. 7671 // This will create the dataset and mark the zone 'installed'. 7672 var args; 7673 7674 if (reprovisioning) { 7675 // reprovisioning we do *most* of install, but not this. 7676 cb(); 7677 return; 7678 } 7679 7680 args = ['-z', vmobj.zonename, 'install', '-q', 7681 payload.quota.toString()]; 7682 7683 // For both OS and KVM VMs you can pass an image_uuid at the 7684 // top-level. This will be your zone's root dataset. On KVM the user 7685 // is never exposed to this. It's used there for something like 7686 // SPICE. 7687 if (payload.hasOwnProperty('image_uuid')) { 7688 args.push('-t', payload.image_uuid, '-x', 'nodataset'); 7689 } 7690 7691 zoneadm(args, log, function (err, fds) { 7692 if (err) { 7693 log.error({err: err, stdout: fds.stdout, 7694 stderr: fds.stderr}, 'zoneadm failed to install: ' 7695 + err.message); 7696 cb(err); 7697 } else { 7698 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 7699 'zoneadm installed zone'); 7700 cb(); 7701 } 7702 }); 7703 }, function (cb) { 7704 // Apply compression if set 7705 var args = []; 7706 if (payload.hasOwnProperty('zfs_root_compression')) { 7707 args = ['set', 'compression=' 7708 + payload.zfs_root_compression, payload.zfs_filesystem]; 7709 zfs(args, log, function (err) { 7710 cb(err); 7711 }); 7712 } else { 7713 cb(); 7714 } 7715 }, function (cb) { 7716 // Apply recsize if set 7717 var args = []; 7718 if (payload.hasOwnProperty('zfs_root_recsize')) { 7719 args = ['set', 'recsize=' + payload.zfs_root_recsize, 7720 payload.zfs_filesystem]; 7721 zfs(args, log, function (err) { 7722 cb(err); 7723 }); 7724 } else { 7725 cb(); 7726 } 7727 }, function (cb) { 7728 // Some zones can have an additional 'data' dataset delegated to 7729 // them for use in the zone. This will set that up. If the option 7730 // is not set, the following does nothing. 7731 if (!receiving && !reprovisioning) { 7732 createDelegatedDataset(payload, log, function (err) { 7733 if (err) { 7734 cb(err); 7735 } else { 7736 cb(); 7737 } 7738 }); 7739 } else { 7740 cb(); 7741 } 7742 }, function (cb) { 7743 // Write out the zone's metadata 7744 // Note: we don't do this when receiving because dataset will 7745 // already contain metadata and we don't want to wipe that out. 7746 if (!receiving && !reprovisioning) { 7747 saveMetadata(payload, log, function (err) { 7748 if (err) { 7749 log.error(err, 'unable to save metadata: ' 7750 + err.message); 7751 cb(err); 7752 } else { 7753 cb(); 7754 } 7755 }); 7756 } else { 7757 cb(); 7758 } 7759 }, function (cb) { 7760 // Write out the zone's routes 7761 // Note: we don't do this when receiving because dataset will 7762 // already contain routes and we don't want to wipe that out. 7763 if (!receiving && !reprovisioning) { 7764 saveRoutes(payload, log, function (err) { 7765 if (err) { 7766 log.error(err, 'unable to save routes: ' 7767 + err.message); 7768 cb(err); 7769 } else { 7770 cb(); 7771 } 7772 }); 7773 } else { 7774 cb(); 7775 } 7776 }, function (cb) { 7777 // if we were receiving, we're done receiving now 7778 if (receiving) { 7779 VM.unsetTransition(vmobj, {log: log}, cb); 7780 } else { 7781 cb(); 7782 } 7783 }, function (cb) { 7784 // var zoneinit is in installZone() scope 7785 7786 // when receiving zoneinit is never run. 7787 if (receiving) { 7788 cb(); 7789 return; 7790 } 7791 7792 getZoneinitJSON(vmobj.zonepath, log, function (zoneinit_err, data) { 7793 7794 if (zoneinit_err) { 7795 // NOTE: not existing is not going to give us a zoneinit_err 7796 log.warn(zoneinit_err, 'error in getZoneinitJSON'); 7797 cb(zoneinit_err); 7798 return; 7799 } 7800 7801 if (data) { 7802 zoneinit = data; 7803 } else { 7804 zoneinit = {}; 7805 } 7806 7807 cb(); 7808 }); 7809 }, function (cb) { 7810 // var_svc_provisioning is at installZone() scope 7811 7812 // If we're not receiving, we're provisioning a new VM and in that 7813 // case we write the /var/svc/provisioning file which should exist 7814 // until something in the zone decides provisioning is complete. At 7815 // that point it will be moved to either: 7816 // 7817 // /var/svc/provision_success 7818 // /var/svc/provision_failure 7819 // 7820 // to indicate that the provisioning setup has been completed. 7821 7822 if (receiving) { 7823 cb(); 7824 return; 7825 } 7826 7827 fs.writeFile(path.join(vmobj.zonepath, 'root', 7828 '/var/svc/provisioning'), '', function (err, result) { 7829 7830 if (err) { 7831 log.error(err, 'failed to create ' 7832 + '/var/svc/provisioning: ' + err.message); 7833 } else { 7834 log.debug('created /var/svc/provisioning in ' 7835 + path.join(vmobj.zonepath, 'root')); 7836 } 7837 7838 cb(err); 7839 }); 7840 }, function (cb) { 7841 // For joyent and joyent-minimal at least, set the timeout for the 7842 // svc start method to the value specified in the payload, or a 7843 // default. 7844 7845 var timeout; 7846 7847 if (BRAND_OPTIONS[vmobj.brand].features.update_mdata_exec_timeout) { 7848 7849 if (payload.hasOwnProperty('mdata_exec_timeout')) { 7850 timeout = payload.mdata_exec_timeout; 7851 } else { 7852 timeout = DEFAULT_MDATA_TIMEOUT; 7853 } 7854 7855 svccfg(vmobj.zonepath, [ 7856 '-s', 'svc:/smartdc/mdata:execute', 7857 'setprop', 'start/timeout_seconds', '=', 'count:', timeout 7858 ], log, function (error, stdio) { 7859 7860 if (error) { 7861 log.error(error, 'failed to set mdata:exec timeout'); 7862 cb(error); 7863 return; 7864 } 7865 7866 cb(); 7867 }); 7868 } else { 7869 cb(); 7870 } 7871 7872 }, function (cb) { 7873 // This writes out the 'zoneconfig' file used by zoneinit to root's 7874 // home directory in the zone. 7875 if (! receiving 7876 && BRAND_OPTIONS[vmobj.brand].features.zoneinit 7877 && (! zoneinit.hasOwnProperty('features') 7878 || zoneinit.features.zoneconfig)) { 7879 7880 // No 'features' means old dataset. If we have old dataset or 7881 // one that really wants a zoneconfig, write it out. 7882 7883 writeZoneconfig(payload, log, function (err) { 7884 cb(err); 7885 }); 7886 } else { 7887 cb(); 7888 } 7889 }, function (cb) { 7890 if (BRAND_OPTIONS[vmobj.brand].features.write_zone_netfiles 7891 && !receiving) { 7892 7893 writeZoneNetfiles(payload, log, function (err) { 7894 cb(err); 7895 }); 7896 } else { 7897 cb(); 7898 } 7899 }, function (cb) { 7900 if (vmobj.hasOwnProperty('zonepath') 7901 && BRAND_OPTIONS[vmobj.brand].features.cleanup_dataset 7902 && !receiving) { 7903 7904 cleanupMessyDataset(vmobj.zonepath, vmobj.brand, log, 7905 function (err) { 7906 7907 cb(err); 7908 }); 7909 } else { 7910 cb(); 7911 } 7912 }, function (cb) { 7913 // Firewall data has not changed when reprovisioning, so we don't 7914 // re-run addFirewallData() 7915 if (reprovisioning) { 7916 cb(); 7917 return; 7918 } 7919 7920 // Add firewall data if it was included 7921 addFirewallData(payload, vmobj, log, cb); 7922 }, function (cb) { 7923 7924 var cancel; 7925 var calledback = false; 7926 var prov_wait = true; 7927 // var_svc_provisioning is at installZone() scope 7928 7929 // The vm is now ready to start, we'll start if autoboot is set. If 7930 // not, we also don't want to wait for 'provisioning'. 7931 if (!payload.autoboot) { 7932 cb(); 7933 return; 7934 } 7935 7936 // In these cases we never wait for provisioning -> running 7937 if (payload.nowait || receiving || vmobj.state !== 'provisioning') { 7938 prov_wait = false; 7939 } 7940 7941 // most VMs support the /var/svc/provision{ing,_success,_failure} 7942 // files. For those, if !nowait, we wait for the file to change 7943 // from provisioning -> either provision_success, or 7944 // provision_failure. 7945 7946 if (prov_wait) { 7947 // wait for /var/svc/provisioning -> provision_success/failure 7948 cancel = VM.waitForProvisioning(vmobj, {log: log}, 7949 function (err) { 7950 7951 log.debug(err, 'waited for provisioning'); 7952 7953 if (!err) { 7954 log.info('provisioning complete: ' 7955 + '/var/svc/provisioning is gone'); 7956 // this will clear the provision transition 7957 VM.unsetTransition(vmobj, {log: log}, 7958 function (unset_err) { 7959 7960 if (unset_err) { 7961 log.error(unset_err, 'error unsetting ' 7962 + 'transition: ' + unset_err.message); 7963 } 7964 // this and the cb in the VM.start callback might 7965 // both run if we don't check this. 7966 if (!calledback) { 7967 calledback = true; 7968 cb(unset_err); 7969 } 7970 }); 7971 } else { 7972 // failed but might not be able to cb if VM.start's 7973 // callback already did. 7974 log.error(err, 'error waiting for provisioning: ' 7975 + err.message); 7976 // this and the cb in the VM.start callback might 7977 // both run if we don't check this. 7978 if (!calledback) { 7979 calledback = true; 7980 cb(err); 7981 } 7982 } 7983 }); 7984 } 7985 7986 VM.start(payload.uuid, {}, {log: log}, function (err, res) { 7987 if (err) { 7988 // we failed to start so we'll never see provisioning, so 7989 // cancel that and return the error. 7990 if (cancel) { 7991 log.info('cancelling VM.waitForProvisioning'); 7992 cancel(); 7993 } 7994 // this and the cb in the VM.waitForProvisioning 7995 // callback might both run if we don't check this. 7996 if (!calledback) { 7997 calledback = true; 7998 cb(err); 7999 } 8000 return; 8001 } 8002 // if we're waiting for 'provisioning' VM.waitForProvisioning's 8003 // callback will call cb(). If we're not going to wait, we call 8004 // it here. 8005 if (!prov_wait) { 8006 // this and the cb in the VM.waitForProvisioning 8007 // callback might both run if we don't check this. 8008 if (!calledback) { 8009 calledback = true; 8010 cb(); 8011 } 8012 } 8013 }); 8014 }], function (error) { 8015 callback(error); 8016 } 8017 ); 8018 } 8019 8020 function getZoneinitJSON(rootpath, log, cb) 8021 { 8022 var filename; 8023 var zoneroot; 8024 8025 assert(log, 'no logger passed to getZoneinitJSON()'); 8026 8027 zoneroot = path.join('/', rootpath, 'root'); 8028 filename = path.join(zoneroot, '/var/zoneinit/zoneinit.json'); 8029 8030 try { 8031 assertSafeZonePath(zoneroot, '/var/zoneinit/zoneinit.json', 8032 {type: 'file', enoent_ok: true}); 8033 } catch (e) { 8034 log.error(e, 'Error validating /var/zoneinit/zoneinit.json: ' 8035 + e.message); 8036 cb(e); 8037 return; 8038 } 8039 8040 fs.readFile(filename, function (error, data) { 8041 var zoneinit; 8042 8043 if (error && (error.code === 'ENOENT')) { 8044 // doesn't exist, leave empty 8045 log.debug('zoneinit.json does not exist.'); 8046 cb(); 8047 } else if (error) { 8048 // error reading: fail. 8049 cb(error); 8050 } else { 8051 // success try to load json 8052 try { 8053 zoneinit = JSON.parse(data.toString()); 8054 log.debug({'zoneinit_json': zoneinit}, 8055 'parsed zoneinit.json'); 8056 cb(null, zoneinit); 8057 } catch (e) { 8058 cb(e); 8059 } 8060 } 8061 }); 8062 } 8063 8064 function getDatasetMountpoint(dataset, log, callback) 8065 { 8066 var args; 8067 var cmd = '/usr/sbin/zfs'; 8068 var mountpoint; 8069 8070 assert(log, 'no logger passed to getDatasetMountpoint()'); 8071 8072 args = ['get', '-H', '-o', 'value', 'mountpoint', dataset]; 8073 8074 log.debug(cmd + ' ' + args.join(' ')); 8075 execFile(cmd, args, function (error, stdout, stderr) { 8076 if (error) { 8077 log.error(error, 'zfs get failed with: ' + stderr); 8078 callback(error); 8079 } else { 8080 mountpoint = stdout.replace(/\n/g, ''); 8081 log.debug('mountpoint: "' + mountpoint + '"'); 8082 callback(null, mountpoint); 8083 } 8084 }); 8085 } 8086 8087 // TODO: pull data out of the massive zfs list we pulled earlier 8088 function checkDatasetProvisionable(payload, log, callback) 8089 { 8090 var dataset; 8091 8092 assert(log, 'no logger passed to checkDatasetProvisionable()'); 8093 8094 if (BRAND_OPTIONS[payload.brand].features.var_svc_provisioning) { 8095 // when the brand always supports /var/svc/provisioning we don't have to 8096 // worry about the dataset not supporting it. 8097 callback(true); 8098 return; 8099 } 8100 8101 if (!payload.hasOwnProperty('zpool') 8102 || !payload.hasOwnProperty('image_uuid')) { 8103 8104 log.error('missing properties required to find dataset: ' 8105 + JSON.stringify(payload)); 8106 callback(false); 8107 return; 8108 } 8109 8110 dataset = payload.zpool + '/' + payload.image_uuid; 8111 8112 getDatasetMountpoint(dataset, log, function (dataset_err, mountpoint) { 8113 if (dataset_err) { 8114 log.error('unable to find mount point for ' + dataset); 8115 callback(false); 8116 return; 8117 } 8118 8119 getZoneinitJSON(dataset, log, function (zoneinit_err, zoneinit) { 8120 var filename_1_6_x; 8121 var filename_1_8_x; 8122 8123 if (zoneinit_err) { 8124 log.error(zoneinit_err, 'getZoneinitJSON() failed, assuming ' 8125 + 'not provisionable.'); 8126 callback(false); 8127 return; 8128 } else if (!zoneinit) { 8129 log.debug('no data from getZoneinitJSON(), using {}'); 8130 zoneinit = {}; 8131 } 8132 8133 if (zoneinit.hasOwnProperty('features')) { 8134 if (zoneinit.features.var_svc_provisioning) { 8135 log.info('zoneinit.features.var_svc_provisioning is ' 8136 + 'set.'); 8137 callback(true); 8138 return; 8139 } 8140 // we have features but not var_svc_provisioning === true means 8141 // we can't provision. Fall through and return false. 8142 } else { 8143 // Didn't load zoneinit features, so check for datasets that 8144 // have // 04-mdata.sh. For 1.6.x and earlier datasets this was 8145 // in /root but in 1.8.0 and 1.8.1 it is in /var/zoneinit. For 8146 // 1.8.2 and later we'll not get here as the zoneinit.json will 8147 // exist and we'll use that. 8148 filename_1_6_x = path.join(mountpoint, 'root', 8149 '/root/zoneinit.d/04-mdata.sh'); 8150 filename_1_8_x = path.join(mountpoint, 'root', 8151 '/var/zoneinit/includes/04-mdata.sh'); 8152 8153 if (fs.existsSync(filename_1_6_x)) { 8154 log.info(filename_1_6_x + ' exists'); 8155 callback(true); 8156 return; 8157 } else { 8158 log.debug(filename_1_6_x + ' does not exist'); 8159 if (fs.existsSync(filename_1_8_x)) { 8160 log.info(filename_1_8_x + ' exists'); 8161 callback(true); 8162 return; 8163 } else { 8164 log.debug(filename_1_8_x + ' does not exist'); 8165 // this was our last chance. 8166 // Fall through and return false. 8167 } 8168 } 8169 } 8170 8171 callback(false); 8172 return; 8173 }); 8174 }); 8175 } 8176 8177 // create and install a 'joyent' or 'kvm' brand zone. 8178 function createZone(payload, log, callback) 8179 { 8180 var create_time; 8181 var n; 8182 var now = new Date; 8183 var primary_found; 8184 var provision_timeout = PROVISION_TIMEOUT; 8185 var t; 8186 var vm_version; 8187 var zcfg; 8188 8189 assert(log, 'no logger passed to createZone()'); 8190 8191 log.debug('createZone()'); 8192 8193 payload.zfs_filesystem = payload.zpool + '/' + payload.zonename; 8194 payload.zonepath = '/' + payload.zfs_filesystem; 8195 8196 // we add create-timestamp in all cases except where we're receiving since 8197 // in that case we want to preserve the original create-timestamp. 8198 if (!payload.hasOwnProperty('transition') 8199 || (payload.transition.transition !== 'receiving') 8200 || !payload.hasOwnProperty('create_timestamp')) { 8201 8202 create_time = now.toISOString(); 8203 } else { 8204 create_time = payload.create_timestamp; 8205 } 8206 8207 // we add vm-version (property v) in all cases except where we're receiving 8208 // since in that case we want to preserve the original version. 8209 if (!payload.hasOwnProperty('transition') 8210 || (payload.transition.transition !== 'receiving') 8211 || !payload.hasOwnProperty('v')) { 8212 8213 vm_version = 1; 8214 } else { 8215 vm_version = payload.v; 8216 } 8217 8218 // set the properties that can't be updated later here. 8219 zcfg = 'create -b\n' 8220 + 'set zonepath=' + payload.zonepath + '\n' 8221 + 'set brand=' + payload.brand + '\n' 8222 + 'set uuid=' + payload.uuid + '\n' 8223 + 'set ip-type=exclusive\n' 8224 + 'add attr; set name="vm-version"; set type=string; set value="' 8225 + vm_version + '"; end\n' 8226 + 'add attr; set name="create-timestamp"; set type=string; set value="' 8227 + create_time + '"; end\n'; 8228 8229 if (payload.hasOwnProperty('transition')) { 8230 // IMPORTANT: this is for internal use only and should not be documented 8231 // as an option for create's payload. Used for receive. 8232 t = payload.transition; 8233 zcfg = zcfg 8234 + buildTransitionZonecfg(t.transition, t.target, t.timeout) + '\n'; 8235 } else { 8236 // Assume this is really a new VM, add transition called 'provisioning' 8237 // only if the machine is going to be booting. 8238 if (!payload.hasOwnProperty('autoboot') || payload.autoboot) { 8239 zcfg = zcfg + buildTransitionZonecfg('provisioning', 'running', 8240 provision_timeout * 1000) + '\n'; 8241 } 8242 } 8243 8244 // We call the property 'dataset-uuid' even though the property name is 8245 // image_uuid because existing VMs in the wild will be using dataset-uuid 8246 // already, and we are the point where the image becomes a dataset anyway. 8247 if (payload.hasOwnProperty('image_uuid')) { 8248 zcfg = zcfg + 'add attr; set name="dataset-uuid"; set type=string; ' 8249 + 'set value="' + payload.image_uuid + '"; end\n'; 8250 } 8251 8252 if (BRAND_OPTIONS[payload.brand].features.use_vm_autoboot) { 8253 // we always set autoboot=false for VM zones, since we want vmadmd to 8254 // boot them and not the zones tools. Use vm-autoboot to control VMs 8255 zcfg = zcfg + 'set autoboot=false\n'; 8256 } 8257 8258 // ensure that we have a primary nic, even if one wasn't specified 8259 if (payload.hasOwnProperty('add_nics') && payload.add_nics.length != 0) { 8260 primary_found = false; 8261 8262 for (n in payload.add_nics) { 8263 n = payload.add_nics[n]; 8264 if (n.hasOwnProperty('primary') && n.primary) { 8265 primary_found = true; 8266 break; 8267 } 8268 } 8269 if (!primary_found) { 8270 payload.add_nics[0].primary = true; 8271 } 8272 } 8273 8274 // Passing an empty first parameter here, tells buildZonecfgUpdate that 8275 // we're talking about a new machine. 8276 zcfg = zcfg + buildZonecfgUpdate({}, payload, log); 8277 8278 // include the zonecfg in the debug output to help track down problems. 8279 log.debug(zcfg); 8280 8281 // send the zonecfg data we just generated as a file to zonecfg, 8282 // this will create the zone. 8283 zonecfgFile(zcfg, ['-z', payload.zonename], log, function (err, fds) { 8284 if (err || payload.create_only) { 8285 log.error({err: err, zcfg: zcfg, stdout: fds.stdout, 8286 stderr: fds.stderr}, 'failed to modify zonecfg'); 8287 callback(err); 8288 } else { 8289 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8290 'modified zonecfg'); 8291 installZone(payload, log, callback); 8292 } 8293 }); 8294 } 8295 8296 function normalizeNics(payload, vmobj) 8297 { 8298 var n; 8299 var nic; 8300 8301 // ensure all NICs being created/added have a MAC, remove the 'index' if it 8302 // is passed (that's deprecated), rename 'interface' to 'physical'. 8303 if (payload.hasOwnProperty('add_nics')) { 8304 for (n in payload.add_nics) { 8305 if (payload.add_nics.hasOwnProperty(n)) { 8306 nic = payload.add_nics[n]; 8307 8308 if (!nic.hasOwnProperty('mac') 8309 && !nic.hasOwnProperty('vrrp_vrid')) { 8310 nic.mac = generateMAC(); 8311 } 8312 delete nic.index; 8313 if (nic.hasOwnProperty('interface')) { 8314 nic.physical = nic.interface; 8315 delete nic.interface; 8316 } 8317 8318 // nics.*.primary only supports true value, unset false. We also 8319 // handle the case here why they used the deprecated '1' value. 8320 // We will have already warned them, but still support for now. 8321 if (nic.hasOwnProperty('primary')) { 8322 if (nic.primary || nic.primary === '1' 8323 || nic.primary === 1) { 8324 8325 nic.primary = true; 8326 } else { 8327 delete nic.primary; 8328 } 8329 } 8330 } 8331 } 8332 } 8333 } 8334 8335 /* 8336 * This is called for both create and update, everything here should be safe for 8337 * both. The vmobj will be set if it's an update. 8338 * 8339 */ 8340 function normalizePayload(payload, vmobj, log, callback) 8341 { 8342 var action; 8343 var allowed; 8344 var brand; 8345 var property; 8346 8347 assert(log, 'no logger passed to normalizePayload()'); 8348 8349 // fix type of arguments that should be numbers, do this here so that fixing 8350 // memory works correctly later using math. 8351 for (property in payload) { 8352 if (payload.hasOwnProperty(property)) { 8353 if (PAYLOAD_PROPERTIES.hasOwnProperty(property) 8354 && PAYLOAD_PROPERTIES[property].type === 'integer' 8355 && payload[property] !== undefined) { 8356 // undefined is a special case since we use that to unset props 8357 8358 payload[property] = Number(payload[property]); 8359 if (isNaN(payload[property])) { 8360 callback(new Error('Invalid value for ' + property + ': ' 8361 + JSON.stringify(payload[property]) + ':' 8362 + typeof (payload[property]))); 8363 return; 8364 } 8365 } 8366 } 8367 } 8368 8369 if (payload.hasOwnProperty('quota') && payload.quota === undefined) { 8370 // when unsetting quota we set to 0 8371 payload.quota = 0; 8372 } 8373 8374 if (vmobj) { 8375 /* update */ 8376 fixPayloadMemory(payload, vmobj, log); 8377 action = 'update'; 8378 } else { 8379 /* this also calls fixPayloadMemory() */ 8380 applyZoneDefaults(payload, log); 8381 8382 if (payload.hasOwnProperty('create_only') 8383 && payload.transition.transition === 'receiving') { 8384 8385 action = 'receive'; 8386 } else { 8387 action = 'create'; 8388 } 8389 } 8390 8391 // Should always have a brand after we applied defaults. 8392 if (vmobj && vmobj.hasOwnProperty('brand')) { 8393 brand = vmobj.brand; 8394 } else if (payload.hasOwnProperty('brand')) { 8395 brand = payload.brand; 8396 } else { 8397 callback(new Error('Unable to determine brand for payload')); 8398 return; 8399 } 8400 8401 // Historically we supported dataset_uuid for joyent+joyent-minimal and 8402 // zone_dataset_uuid for kvm. Now we just support image_uuid so give a 8403 // deprecation warning and translate if old version specified. This needs 8404 // to happen before VM.validate because image_uuid is required for most 8405 // VMs. 8406 allowed = BRAND_OPTIONS[brand].allowed_properties; 8407 if ((allowed.hasOwnProperty('dataset_uuid') 8408 && payload.hasOwnProperty('dataset_uuid')) 8409 || (allowed.hasOwnProperty('zone_dataset_uuid') 8410 && payload.hasOwnProperty('zone_dataset_uuid'))) { 8411 8412 property = (payload.hasOwnProperty('dataset_uuid') ? 'dataset_uuid' 8413 : 'zone_dataset_uuid'); 8414 8415 if (payload.hasOwnProperty('image_uuid')) { 8416 log.warn('DEPRECATED option ' + property + ' found, ' 8417 + 'ignoring. In the future use image_uuid only.'); 8418 } else { 8419 log.warn('DEPRECATED option ' + property + ' found, ' 8420 + 'ignoring. In the future use image_uuid instead.'); 8421 payload.image_uuid = payload[property]; 8422 delete payload.dataset_uuid; 8423 } 8424 } 8425 8426 // after ZoneDefaults have been applied, we should always have zone. Now 8427 // we validate the payload properties and remove any that are invalid. If 8428 // there are bad values we'll just fail. 8429 VM.validate(brand, action, payload, {log: log}, function (errors) { 8430 var bad_prop; 8431 var compound_props = ['disks', 'nics', 'filesystems']; 8432 var matches; 8433 var obj; 8434 var prop; 8435 8436 if (errors) { 8437 if (errors.hasOwnProperty('bad_brand')) { 8438 callback(new Error('Invalid brand while validating payload: ' 8439 + JSON.stringify(brand))); 8440 return; 8441 } 8442 if (errors.bad_values.length > 0) { 8443 callback(new Error('Invalid value(s) for: ' 8444 + errors.bad_values.join(','))); 8445 return; 8446 } 8447 if (errors.missing_properties.length > 0) { 8448 callback(new Error('Missing required properties: ' 8449 + errors.missing_properties.join(','))); 8450 return; 8451 } 8452 for (bad_prop in errors.bad_properties) { 8453 bad_prop = errors.bad_properties[bad_prop]; 8454 log.warn('Warning, invalid ' + action + ' property: [' 8455 + bad_prop + '] removing from payload.'); 8456 8457 // for bad properties like nics.*.allow_unfiltered_promisc we 8458 // need to remove it from add_nics, update_nics, etc. 8459 for (prop in compound_props) { 8460 prop = compound_props[prop]; 8461 8462 matches = new RegExp('^' + prop 8463 + '\\.\\*\\.(.*)$').exec(bad_prop); 8464 if (matches) { 8465 if (payload.hasOwnProperty(prop)) { 8466 for (obj in payload[prop]) { 8467 delete payload[prop][obj][matches[1]]; 8468 } 8469 } 8470 if (payload.hasOwnProperty('add_' + prop)) { 8471 for (obj in payload['add_' + prop]) { 8472 delete payload['add_' + prop][obj][matches[1]]; 8473 } 8474 } 8475 if (payload.hasOwnProperty('update_' + prop)) { 8476 for (obj in payload['update_' + prop]) { 8477 delete payload['update_' 8478 + prop][obj][matches[1]]; 8479 } 8480 } 8481 } 8482 } 8483 8484 delete payload[bad_prop]; 8485 } 8486 } 8487 8488 // By the time we got here all the properties in the payload are allowed 8489 8490 // Now we make sure we've got a zonename (use uuid if not already set) 8491 if (!payload.hasOwnProperty('zonename') 8492 || payload.zonename === undefined) { 8493 8494 payload.zonename = payload.uuid; 8495 } 8496 8497 // You use 'disks' and 'nics' when creating, but the underlying 8498 // functions expect add_disks and add_nics, so we rename them now that 8499 // we've confirmed we've got the correct thing for this action. 8500 if (payload.hasOwnProperty('disks')) { 8501 if (payload.hasOwnProperty('add_disks')) { 8502 callback(new Error('Cannot specify both "disks" and ' 8503 + '"add_disks"')); 8504 return; 8505 } 8506 payload.add_disks = payload.disks; 8507 delete payload.disks; 8508 } 8509 if (payload.hasOwnProperty('nics')) { 8510 if (payload.hasOwnProperty('add_nics')) { 8511 callback(new Error('Cannot specify both "nics" and ' 8512 + '"add_nics"')); 8513 return; 8514 } 8515 payload.add_nics = payload.nics; 8516 delete payload.nics; 8517 } 8518 if (payload.hasOwnProperty('filesystems')) { 8519 if (payload.hasOwnProperty('add_filesystems')) { 8520 callback(new Error('Cannot specify both "filesystems" and ' 8521 + '"add_filesystems"')); 8522 return; 8523 } 8524 payload.add_filesystems = payload.filesystems; 8525 delete payload.filesystems; 8526 } 8527 8528 // if there's a zfs_root_* and no zfs_data_*, normally the properties 8529 // would fall through, we don't want that. 8530 if (payload.hasOwnProperty('zfs_root_compression') 8531 && !payload.hasOwnProperty('zfs_data_compression')) { 8532 8533 if (vmobj && vmobj.hasOwnProperty('zfs_data_compression')) { 8534 // keep existing value. 8535 payload.zfs_data_compression = vmobj.zfs_data_compression; 8536 } else { 8537 // keep default value. 8538 payload.zfs_data_compression = 'off'; 8539 } 8540 } 8541 if (payload.hasOwnProperty('zfs_root_recsize') 8542 && !payload.hasOwnProperty('zfs_data_recsize')) { 8543 8544 if (vmobj && vmobj.hasOwnProperty('zfs_data_recsize')) { 8545 // keep existing value. 8546 payload.zfs_data_recsize = vmobj.zfs_data_recsize; 8547 } else { 8548 // keep default value. 8549 payload.zfs_data_recsize = 131072; 8550 } 8551 } 8552 8553 // this will ensure we've got a MAC, etc. 8554 normalizeNics(payload, vmobj); 8555 8556 // Fix types for boolean fields in case someone put in 'false'/'true' 8557 // instead of false/true 8558 for (property in payload) { 8559 if (payload.hasOwnProperty(property)) { 8560 if (PAYLOAD_PROPERTIES.hasOwnProperty(property) 8561 && PAYLOAD_PROPERTIES[property].type === 'boolean') { 8562 8563 payload[property] = fixBooleanLoose(payload[property]); 8564 } 8565 } 8566 } 8567 8568 // We used to support zfs_storage_pool_name, but zpool is better. 8569 if (payload.hasOwnProperty('zfs_storage_pool_name')) { 8570 if (payload.hasOwnProperty('zpool')) { 8571 log.warn('DEPRECATED option zfs_storage_pool_name found, ' 8572 + 'ignoring!'); 8573 } else { 8574 log.warn('DEPRECATED option zfs_storage_pool_name found, ' 8575 + 'replacing with zpool!'); 8576 payload.zpool = payload.zfs_storage_pool_name; 8577 delete payload.zfs_storage_pool_name; 8578 } 8579 } 8580 8581 // When creating a VM with SPICE you need the image_uuid, if you don't 8582 // pass that, we'll remove any SPICE options. 8583 if (action === 'create' 8584 && !payload.hasOwnProperty('image_uuid')) { 8585 8586 if (payload.hasOwnProperty('spice_opts') 8587 || payload.hasOwnProperty('spice_password') 8588 || payload.hasOwnProperty('spice_port')) { 8589 8590 log.warn('Creating with SPICE options requires ' 8591 + 'image_uuid, REMOVING spice_*'); 8592 delete payload.spice_opts; 8593 delete payload.spice_password; 8594 delete payload.spice_port; 8595 } 8596 } 8597 8598 checkPayloadProperties(payload, vmobj, log, function (e) { 8599 if (e) { 8600 callback(e); 8601 } else { 8602 callback(); 8603 } 8604 }); 8605 }); 8606 } 8607 8608 function buildTransitionZonecfg(transition, target, timeout) 8609 { 8610 var cmdline; 8611 8612 cmdline = 'add attr; set name=transition; set value="' 8613 + transition + ':' + target + ':' + (Date.now(0) + timeout).toString() 8614 + '"; set type=string; end'; 8615 8616 return cmdline; 8617 } 8618 8619 // vmobj should have: 8620 // 8621 // uuid 8622 // transition_to (if set) 8623 // 8624 exports.unsetTransition = function (vmobj, options, callback) 8625 { 8626 var log; 8627 8628 // options is optional 8629 if (arguments.length === 2) { 8630 callback = arguments[1]; 8631 options = {}; 8632 } 8633 8634 ensureLogging(true); 8635 if (options.hasOwnProperty('log')) { 8636 log = options.log; 8637 } else { 8638 log = VM.log.child({action: 'unsetTransition', vm: vmobj.uuid}); 8639 } 8640 8641 zonecfg(['-u', vmobj.uuid, 'remove -F attr name=transition'], log, 8642 function (err, fds) { 8643 8644 if (err) { 8645 // log at info because this might be because already removed 8646 log.info({err: err, stdout: fds.stdout, stderr: fds.stderr}, 8647 'unable to remove transition for zone ' + vmobj.uuid); 8648 } else { 8649 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8650 'removed transition for zone ' + vmobj.uuid); 8651 } 8652 8653 zonecfg(['-u', vmobj.uuid, 'info attr name=transition'], log, 8654 function (info_err, info_fds) { 8655 8656 if (info_err) { 8657 log.error({err: info_err, stdout: info_fds.stdout, 8658 stderr: info_fds.stderr}, 8659 'failed to confirm transition removal'); 8660 callback(info_err); 8661 return; 8662 } 8663 8664 if (info_fds.stdout !== 'No such attr resource.\n') { 8665 log.error({stdout: info_fds.stdout, stderr: info_fds.stderr}, 8666 'unknown error checking transition after removal'); 8667 callback(new Error('transition does not appear to have been ' 8668 + 'removed zonecfg said: ' + JSON.stringify(info_fds))); 8669 return; 8670 } 8671 8672 // removed the transition, now attempt to start if we're rebooting. 8673 if (vmobj.transition_to && vmobj.transition_to === 'start') { 8674 log.debug('VM ' + vmobj.uuid + ' was stopping for reboot, ' 8675 + 'transitioning to start.'); 8676 VM.start(vmobj.uuid, {}, {log: log}, function (e) { 8677 if (e) { 8678 log.error(e, 'failed to start when clearing ' 8679 + 'transition'); 8680 } 8681 callback(); 8682 }); 8683 } else { 8684 callback(); 8685 } 8686 }); 8687 }); 8688 }; 8689 8690 // 8691 // vmobj fields used: 8692 // 8693 // transition 8694 // uuid 8695 // 8696 function setTransition(vmobj, transition, target, timeout, log, callback) 8697 { 8698 assert(log, 'no logger passed to setTransition()'); 8699 8700 if (!timeout) { 8701 callback(new Error('setTransition() requires timeout argument.')); 8702 return; 8703 } 8704 8705 async.series([ 8706 function (cb) { 8707 // unset an existing transition 8708 if (vmobj.hasOwnProperty('transition')) { 8709 VM.unsetTransition(vmobj, {log: log}, cb); 8710 } else { 8711 cb(); 8712 } 8713 }, function (cb) { 8714 var zcfg; 8715 8716 zcfg = buildTransitionZonecfg(transition, target, timeout); 8717 zonecfg(['-u', vmobj.uuid, zcfg], log, function (err, fds) { 8718 if (err) { 8719 log.error({err: err, stdout: fds.stdout, 8720 stderr: fds.stderr}, 'failed to set transition=' 8721 + transition + ' for VM ' + vmobj.uuid); 8722 } else { 8723 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8724 'set transition=' + transition + ' for vm ' 8725 + vmobj.uuid); 8726 } 8727 8728 cb(err); 8729 }); 8730 } 8731 ], function (error) { 8732 callback(error); 8733 }); 8734 } 8735 8736 function receiveVM(json, log, callback) 8737 { 8738 var payload = {}; 8739 8740 assert(log, 'no logger passed to receiveVM()'); 8741 8742 try { 8743 payload = JSON.parse(json); 8744 } catch (e) { 8745 callback(e); 8746 return; 8747 } 8748 8749 payload.create_only = true; 8750 8751 // adding transition here is considered to be *internal only* not for 8752 // consumer use and not to be documented as a property you can use with 8753 // create. 8754 payload.transition = 8755 {'transition': 'receiving', 'target': 'stopped', 'timeout': 86400}; 8756 8757 // We delete tags and metadata here becasue this exists in the root 8758 // dataset which we will be copying, so it would be duplicated here. 8759 delete payload.customer_metadata; 8760 delete payload.internal_metadata; 8761 delete payload.tags; 8762 8763 // On receive we need to make sure that we don't create new disks so we 8764 // mark them all as nocreate. We also can't set the block_size of imported 8765 // volumes, so we remove that. 8766 if (payload.hasOwnProperty('disks')) { 8767 var disk_idx; 8768 8769 for (disk_idx in payload.disks) { 8770 payload.disks[disk_idx].nocreate = true; 8771 8772 if (payload.disks[disk_idx].image_uuid) { 8773 delete payload.disks[disk_idx].block_size; 8774 } 8775 } 8776 } 8777 8778 VM.create(payload, {log: log}, function (err, result) { 8779 if (err) { 8780 callback(err); 8781 } 8782 8783 // don't include the special transition in the payload we write out. 8784 delete payload.transition; 8785 8786 fs.writeFile('/etc/zones/' + payload.uuid + '-receiving.json', 8787 JSON.stringify(payload, null, 2), function (e) { 8788 8789 if (e) { 8790 callback(e); 8791 return; 8792 } 8793 8794 // ready for datasets 8795 callback(null, result); 8796 }); 8797 }); 8798 } 8799 8800 function receiveStdinChunk(type, log, callback) 8801 { 8802 var child; 8803 var chunk_name = ''; 8804 var chunk_size = 0; 8805 var json = ''; 8806 var remaining = ''; 8807 8808 assert(log, 'no logger passed to receiveStdinChunk()'); 8809 8810 /* 8811 * XXX 8812 * 8813 * node 0.6.x removed support for arbitrary file descriptors which 8814 * means we can only handle stdin for now since we need to pass this 8815 * descriptor directly to the child. 0.8.x is supposed to reintroduce 8816 * this functionality. When we do, this should be changed to open 8817 * the file and set fd to the descriptor, and we should be able to 8818 * get rid of vmunbundle. 8819 * 8820 */ 8821 8822 if (type === 'JSON') { 8823 log.info('/usr/vm/sbin/vmunbundle json'); 8824 child = spawn('/usr/vm/sbin/vmunbundle', ['json'], 8825 {customFds: [0, -1, -1]}); 8826 } else if (type === 'DATASET') { 8827 log.info('/usr/vm/sbin/vmunbundle dataset'); 8828 child = spawn('/usr/vm/sbin/vmunbundle', ['dataset'], 8829 {customFds: [0, -1, -1]}); 8830 } else { 8831 callback(new Error('Unsupported chunk type ' + type)); 8832 } 8833 8834 child.stderr.on('data', function (data) { 8835 var idx; 8836 var line; 8837 var matches; 8838 8839 remaining += data.toString(); 8840 8841 idx = remaining.indexOf('\n'); 8842 while (idx > -1) { 8843 line = trim(remaining.substring(0, idx)); 8844 remaining = remaining.substring(idx + 1); 8845 8846 log.debug('VMUNBUNDLE: ' + line); 8847 matches = line.match(/Size: ([\d]+)/); 8848 if (matches) { 8849 chunk_size = Number(matches[1]); 8850 } 8851 matches = line.match(/Name: \[(.*)\]/); 8852 if (matches) { 8853 chunk_name = matches[1]; 8854 } 8855 8856 idx = remaining.indexOf('\n'); 8857 } 8858 }); 8859 8860 child.stdout.on('data', function (data) { 8861 json += data.toString(); 8862 log.debug('json size is ' + json.length); 8863 }); 8864 8865 child.on('close', function (code) { 8866 log.debug('vmunbundle process exited with code ' + code); 8867 if (code === 3) { 8868 log.debug('vmbundle: end of bundle.'); 8869 callback(null, 'EOF'); 8870 return; 8871 } else if (code !== 0) { 8872 callback(new Error('vmunbundle exited with code ' + code)); 8873 return; 8874 } 8875 8876 // if it was a dataset, we've now imported it. 8877 // if it was json, we've now got it in the json var. 8878 8879 if (type === 'DATASET') { 8880 log.info('Imported dataset ' + chunk_name); 8881 // delete 'sending' snapshot 8882 zfs(['destroy', '-F', chunk_name + '@sending'], log, 8883 function (err, fds) { 8884 if (err) { 8885 log.warn(err, 'Failed to destroy ' + chunk_name 8886 + '@sending: ' + err.message); 8887 } 8888 callback(); 8889 } 8890 ); 8891 } else if (type === 'JSON' && chunk_name === 'JSON' 8892 && json.length <= chunk_size && json.length > 0) { 8893 8894 receiveVM(json, log, function (e, result) { 8895 if (e) { 8896 callback(e); 8897 return; 8898 } 8899 log.info('Receive returning: ' + JSON.stringify(result)); 8900 callback(null, result); 8901 }); 8902 } else { 8903 log.debug('type: [' + type + ']'); 8904 log.debug('chunk_name: [' + chunk_name + ']'); 8905 log.debug('chunk_size: [' + chunk_size + ']'); 8906 log.debug('json.length: [' + json.length + ']'); 8907 log.warn('Failed to get ' + type + '!'); 8908 callback(new Error('Failed to get ' + type + '!')); 8909 } 8910 }); 8911 } 8912 8913 exports.receive = function (target, options, callback) 8914 { 8915 var log; 8916 8917 // options is optional 8918 if (arguments.length === 2) { 8919 callback = arguments[1]; 8920 options = {}; 8921 } 8922 8923 ensureLogging(true); 8924 8925 // We don't know anything about this VM yet, so we don't create a 8926 // VM.log.child. 8927 if (options.hasOwnProperty('log')) { 8928 log = options.log; 8929 } else { 8930 log = VM.log; 8931 } 8932 8933 log.info('Receiving VM from: ' + JSON.stringify(target)); 8934 8935 if (target.hasOwnProperty('host') && target.hasOwnProperty('port')) { 8936 // network receive not yet supported either. 8937 callback(new Error('cannot receive from ' + JSON.stringify(target))); 8938 return; 8939 } else if (typeof (target) !== 'string' || target !== '-') { 8940 callback(new Error('cannot receive from ' + JSON.stringify(target))); 8941 return; 8942 } 8943 8944 receiveStdinChunk('JSON', log, function (error, result) { 8945 var eof = false; 8946 8947 if (error) { 8948 callback(error); 8949 return; 8950 } 8951 if (result && result === 'EOF') { 8952 callback(new Error('unable to find JSON in stdin.')); 8953 } else if (result && result.hasOwnProperty('uuid')) { 8954 // VM started receive, now need datasets 8955 8956 // We have JSON, so we can log better now if we need one 8957 if (!options.hasOwnProperty('log')) { 8958 log = VM.log.child({action: 'receive', vm: result.uuid}); 8959 } 8960 8961 log.info('Receiving VM ' + result.uuid); 8962 log.debug('now looking for datasets'); 8963 8964 async.whilst( 8965 function () { return !eof; }, 8966 function (cb) { 8967 receiveStdinChunk('DATASET', log, function (err, res) { 8968 if (err) { 8969 cb(err); 8970 return; 8971 } 8972 if (res === 'EOF') { 8973 eof = true; 8974 } 8975 cb(); 8976 }); 8977 }, function (err) { 8978 if (err) { 8979 callback(err); 8980 return; 8981 } 8982 // no error so we read all the datasets, try an install. 8983 log.info('receive calling VM.install: ' + eof); 8984 VM.install(result.uuid, {log: log}, function (e) { 8985 if (e) { 8986 log.warn(e, 'couldn\'t install VM: ' 8987 + e.message); 8988 } 8989 callback(e, result); 8990 }); 8991 } 8992 ); 8993 } else { 8994 callback(new Error('unable to receive JSON')); 8995 } 8996 }); 8997 }; 8998 8999 exports.reprovision = function (uuid, payload, options, callback) 9000 { 9001 var log; 9002 var provision_timeout = PROVISION_TIMEOUT; 9003 var set_transition = false; 9004 var snapshot; 9005 var vmobj; 9006 9007 // options is optional 9008 if (arguments.length === 3) { 9009 callback = arguments[2]; 9010 options = {}; 9011 } 9012 9013 ensureLogging(true); 9014 if (options.hasOwnProperty('log')) { 9015 log = options.log; 9016 } else { 9017 log = VM.log.child({action: 'reprovision', vm: uuid}); 9018 } 9019 9020 log.info('Reprovisioning VM ' + uuid + ', original payload:\n' 9021 + JSON.stringify(payload, null, 2)); 9022 9023 async.waterfall([ 9024 function (cb) { 9025 VM.load(uuid, { 9026 fields: [ 9027 'brand', 9028 'datasets', 9029 'hostname', 9030 'nics', 9031 'quota', 9032 'state', 9033 'uuid', 9034 'zfs_filesystem', 9035 'zone_state', 9036 'zonename', 9037 'zonepath', 9038 'zpool' 9039 ], 9040 log: log 9041 }, function (err, obj) { 9042 if (err) { 9043 cb(err); 9044 return; 9045 } 9046 vmobj = obj; 9047 log.debug('Loaded VM is: ' + JSON.stringify(vmobj, null, 2)); 9048 cb(); 9049 }); 9050 }, function (cb) { 9051 if (BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 9052 && BRAND_OPTIONS[vmobj.brand].features.reprovision 9053 && BRAND_OPTIONS[vmobj.brand].features.brand_install_script) { 9054 9055 cb(); 9056 } else { 9057 cb(new Error('brand "' + vmobj.brand + '" does not yet support' 9058 + ' reprovision')); 9059 } 9060 }, function (cb) { 9061 // only support image_uuid at top level (for non-KVM currently) 9062 if (!payload.hasOwnProperty('image_uuid')) { 9063 cb(new Error('payload is missing image_uuid')); 9064 } else { 9065 cb(); 9066 } 9067 }, function (cb) { 9068 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 1) { 9069 cb(new Error('cannot support reprovision with multiple ' 9070 + 'delegated datasets')); 9071 return; 9072 } else if (vmobj.hasOwnProperty('datasets') 9073 && vmobj.datasets.length === 1 9074 && vmobj.datasets[0] !== vmobj.zfs_filesystem + '/data') { 9075 9076 cb(new Error('cannot support reprovision with non-standard "' 9077 + vmobj.datasets[0] + '" dataset')); 9078 return; 9079 } 9080 cb(); 9081 }, function (cb) { 9082 // TODO: change here when we support zvols/KVM, add size 9083 // & change type 9084 9085 validateImage({ 9086 type: 'zone-dataset', 9087 uuid: payload.image_uuid, 9088 zpool: vmobj.zpool 9089 }, log, function (e) { 9090 cb(e); 9091 }); 9092 }, function (cb) { 9093 // ensure we're stopped before reprovision starts 9094 if (vmobj.zone_state !== 'installed') { 9095 VM.stop(uuid, {log: log}, function (e) { 9096 if (e) { 9097 log.error(e, 'unable to stop VM ' + uuid + ': ' 9098 + e.message); 9099 } 9100 cb(e); 9101 }); 9102 } else { 9103 cb(); 9104 } 9105 }, function (cb) { 9106 // Set transition to provisioning now, we're going for it. 9107 setTransition(vmobj, 'provisioning', 'running', 9108 (provision_timeout * 1000), log, function (err) { 9109 if (err) { 9110 cb(err); 9111 } else { 9112 set_transition = true; 9113 cb(); 9114 } 9115 }); 9116 }, function (cb) { 9117 // we validated any delegated dataset above, so we just need to 9118 // remove the 'zoned' flag if we've got one. 9119 if (!vmobj.hasOwnProperty('datasets') 9120 || vmobj.datasets.length === 0) { 9121 9122 cb(); 9123 return; 9124 } 9125 zfs(['set', 'zoned=off', vmobj.datasets[0]], log, 9126 function (err, fds) { 9127 9128 if (err) { 9129 log.error({err: err, stdout: fds.stdout, 9130 stderr: fds.stderr}, 'Unable to turn off "zoned" for ' 9131 + vmobj.datasets[0]); 9132 } 9133 cb(err); 9134 }); 9135 }, function (cb) { 9136 // if we have a delegated dataset, rename zones/<uuid>/data 9137 // -> zones/<uuid>-reprovisioning-data 9138 if (!vmobj.hasOwnProperty('datasets') 9139 || vmobj.datasets.length === 0) { 9140 9141 cb(); 9142 return; 9143 } 9144 zfs(['rename', '-f', vmobj.datasets[0], vmobj.zfs_filesystem 9145 + '-reprovisioning-data'], log, function (err, fds) { 9146 9147 if (err) { 9148 log.error({err: err, stdout: fds.stdout, 9149 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9150 + vmobj.datasets[0]); 9151 } 9152 cb(err); 9153 }); 9154 }, function (cb) { 9155 // unmount <zonepath>/cores so dataset is not busy 9156 zfs(['umount', vmobj.zonepath + '/cores'], log, 9157 function (err, fds) { 9158 9159 if (err) { 9160 if (trim(fds.stderr).match(/not a mountpoint$/)) { 9161 log.info('ignoring failure to umount cores which ' 9162 + 'wasn\'t mounted'); 9163 cb(); 9164 return; 9165 } else { 9166 log.error({err: err, stdout: fds.stdout, 9167 stderr: fds.stderr}, 'Unable to umount ' 9168 + vmobj.zonepath + '/cores'); 9169 } 9170 } 9171 cb(err); 9172 }); 9173 }, function (cb) { 9174 // rename <zfs_filesystem> dataset out of the way 9175 zfs(['rename', '-f', vmobj.zfs_filesystem, vmobj.zfs_filesystem 9176 + '-reprovisioning-root'], log, function (err, fds) { 9177 9178 if (err) { 9179 log.error({err: err, stdout: fds.stdout, 9180 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9181 + vmobj.zfs_filesystem); 9182 } 9183 cb(err); 9184 }); 9185 }, function (cb) { 9186 var snapname = vmobj.zpool + '/' + payload.image_uuid + '@final'; 9187 9188 // ensure we've got our snapshot 9189 zfs(['get', '-Ho', 'value', 'type', snapname], log, 9190 function (err, fds) { 9191 9192 if (!err) { 9193 // snapshot already exists, use it 9194 log.debug('snapshot "' + snapname + '" exists'); 9195 snapshot = snapname; 9196 cb(); 9197 return; 9198 } 9199 9200 if (fds.stderr.match(/dataset does not exist/)) { 9201 // we'll use a different one. (falls throught to next func) 9202 cb(); 9203 } else { 9204 cb(err); 9205 } 9206 }); 9207 }, function (cb) { 9208 var snapname; 9209 9210 if (snapshot) { 9211 // already know which one to use, don't create one 9212 cb(); 9213 return; 9214 } 9215 9216 snapname = vmobj.zpool + '/' + payload.image_uuid 9217 + '@' + vmobj.uuid; 9218 9219 // ensure we've got a snapshot 9220 zfs(['get', '-Ho', 'value', 'type', snapname], log, 9221 function (err, fds) { 9222 9223 if (!err) { 9224 // snapshot already exists, use it 9225 log.debug('snapshot "' + snapname + '" exists'); 9226 snapshot = snapname; 9227 cb(); 9228 return; 9229 } 9230 9231 if (fds.stderr.match(/dataset does not exist/)) { 9232 zfs(['snapshot', snapname], log, function (e, snap_fds) { 9233 if (e) { 9234 e.stdout = snap_fds.stdout; 9235 e.stderr = snap_fds.stderr; 9236 log.error(e, 'Failed to create snapshot: ' 9237 + e.message); 9238 } else { 9239 log.debug('created snapshot "' + snapname + '"'); 9240 snapshot = snapname; 9241 } 9242 cb(e); 9243 }); 9244 } else { 9245 cb(err); 9246 return; 9247 } 9248 }); 9249 }, function (cb) { 9250 var args; 9251 9252 // clone the new image creating a new dataset for zoneroot 9253 assert(snapshot); 9254 9255 args = ['clone']; 9256 if (vmobj.hasOwnProperty('quota') && vmobj.quota > 0) { 9257 args.push('-o'); 9258 args.push('quota=' + vmobj.quota + 'G'); 9259 } 9260 args.push(snapshot); 9261 args.push(vmobj.zfs_filesystem); 9262 9263 zfs(args, log, function (err, fds) { 9264 if (err) { 9265 log.error({err: err, stdout: fds.stdout, 9266 stderr: fds.stderr}, 'Unable to create new clone of ' 9267 + payload.image_uuid); 9268 } 9269 cb(err); 9270 }); 9271 }, function (cb) { 9272 var cmd; 9273 9274 // copy zones/<uuid>-reprovisioning-root/config to 9275 // zones/<uuid>/config so we keep metadata and ipf rules. 9276 try { 9277 fs.mkdirSync(vmobj.zonepath + '/config'); 9278 } catch (e) { 9279 if (e.code !== 'EEXIST') { 9280 e.message = 'Unable to recreate ' + vmobj.zonepath 9281 + '/config: ' + e.message; 9282 cb(e); 9283 return; 9284 } 9285 } 9286 9287 cmd = 'cp -pPR ' 9288 + vmobj.zonepath + '-reprovisioning-root/config/* ' 9289 + vmobj.zonepath + '/config/'; 9290 9291 log.debug(cmd); 9292 exec(cmd, function (error, stdout, stderr) { 9293 log.debug({'stdout': stdout, 'stderr': stderr}, 'cp results'); 9294 if (error) { 9295 error.stdout = stdout; 9296 error.stderr = stderr; 9297 cb(error); 9298 return; 9299 } else { 9300 cb(); 9301 } 9302 }); 9303 }, function (cb) { 9304 // destroy <zonepath>-reprovisioning-root, since it's no longer used 9305 zfs(['destroy', '-r', vmobj.zfs_filesystem 9306 + '-reprovisioning-root'], log, function (err, fds) { 9307 9308 if (err) { 9309 log.error({err: err, stdout: fds.stdout, 9310 stderr: fds.stderr}, 'Unable to destroy ' 9311 + vmobj.zfs_filesystem + '-reprovisioning-root: ' 9312 + err.message); 9313 } 9314 cb(err); 9315 }); 9316 }, function (cb) { 9317 // remount /zones/<uuid>/cores 9318 zfs(['mount', vmobj.zpool + '/cores/' + uuid], log, 9319 function (err, fds) { 9320 9321 if (err) { 9322 log.error({err: err, stdout: fds.stdout, 9323 stderr: fds.stderr}, 'Unable to mount ' + vmobj.zonepath 9324 + '/cores: ' + err.message); 9325 } 9326 cb(err); 9327 }); 9328 }, function (cb) { 9329 var args = ['-r', '-R', vmobj.zonepath, '-z', vmobj.zonename]; 9330 var cmd = BRAND_OPTIONS[vmobj.brand].features.brand_install_script; 9331 9332 // We run the brand's install script here with the -r flag which 9333 // tells it to do everything that's relevant to reprovision. 9334 9335 log.debug(cmd + ' ' + args.join(' ')); 9336 execFile(cmd, args, function (error, stdout, stderr) { 9337 var new_err; 9338 9339 if (error) { 9340 new_err = new Error('Error running brand install script ' 9341 + cmd); 9342 // error's message includes stderr. 9343 log.error({err: error, stdout: stdout}, 9344 'brand install script exited with code ' + error.code); 9345 cb(new_err); 9346 } else { 9347 log.debug(cmd + ' stderr:\n' + stderr); 9348 cb(); 9349 } 9350 }); 9351 }, function (cb) { 9352 // rename zones/<uuid>-reprovision-data -> zones/<uuid>/data 9353 if (!vmobj.hasOwnProperty('datasets') 9354 || vmobj.datasets.length === 0) { 9355 9356 cb(); 9357 return; 9358 } 9359 zfs(['rename', '-f', vmobj.zfs_filesystem + '-reprovisioning-data', 9360 vmobj.datasets[0]], log, function (err, fds) { 9361 9362 if (err) { 9363 log.error({err: err, stdout: fds.stdout, 9364 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9365 + vmobj.zfs_filesystem); 9366 } 9367 cb(err); 9368 }); 9369 }, function (cb) { 9370 // set zoned=on for zones/<uuid>/data 9371 if (!vmobj.hasOwnProperty('datasets') 9372 || vmobj.datasets.length === 0) { 9373 9374 cb(); 9375 return; 9376 } 9377 zfs(['set', 'zoned=on', vmobj.datasets[0]], log, 9378 function (err, fds) { 9379 9380 if (err) { 9381 log.error({err: err, stdout: fds.stdout, 9382 stderr: fds.stderr}, 'Unable to set "zoned" for: ' 9383 + vmobj.datasets[0]); 9384 } 9385 cb(err); 9386 }); 9387 }, function (cb) { 9388 // update zone's image_uuid field 9389 var zcfg = 'select attr name=dataset-uuid; set value="' 9390 + payload.image_uuid + '"; end'; 9391 zonecfg(['-u', uuid, zcfg], log, function (err, fds) { 9392 if (err) { 9393 log.error({err: err, stdout: fds.stdout, 9394 stderr: fds.stderr}, 'unable to set image_uuid on VM ' 9395 + uuid); 9396 } 9397 cb(err); 9398 }); 9399 }, function (cb) { 9400 var p = { 9401 autoboot: true, 9402 reprovisioning: true, 9403 uuid: uuid, 9404 zonename: vmobj.zonename, 9405 zonepath: vmobj.zonepath 9406 }; 9407 9408 // NOTE: someday we could allow mdata_exec_timeout in the original 9409 // payload to reprovision and then pass it along here. 9410 9411 // other fields used by installZone() 9412 [ 9413 'dns_domain', 9414 'hostname', 9415 'quota', 9416 'resolvers', 9417 'tmpfs', 9418 'zfs_filesystem', 9419 'zfs_root_compression', 9420 'zfs_root_recsize' 9421 ].forEach(function (k) { 9422 if (vmobj.hasOwnProperty(k)) { 9423 p[k] = vmobj[k]; 9424 } 9425 }); 9426 9427 // nics needs to be called add_nics here 9428 if (vmobj.hasOwnProperty('nics')) { 9429 p.add_nics = vmobj.nics; 9430 } 9431 9432 installZone(p, log, function (err) { 9433 log.debug(err, 'ran installZone() for reprovision'); 9434 cb(err); 9435 }); 9436 } 9437 ], function (err) { 9438 if (err && set_transition) { 9439 // remove transition now, if we failed. 9440 VM.unsetTransition(vmobj, {log: log}, function () { 9441 // err here is original err, we ignore failure to unset because 9442 // nothing we can do about that.. 9443 callback(err); 9444 }); 9445 } else { 9446 callback(err); 9447 } 9448 }); 9449 }; 9450 9451 exports.install = function (uuid, options, callback) 9452 { 9453 var log; 9454 9455 // options is optional 9456 if (arguments.length === 2) { 9457 callback = arguments[1]; 9458 options = {}; 9459 } 9460 9461 ensureLogging(true); 9462 if (options.hasOwnProperty('log')) { 9463 log = options.log; 9464 } else { 9465 log = VM.log.child({action: 'install', vm: uuid}); 9466 } 9467 9468 log.info('Installing VM ' + uuid); 9469 9470 fs.readFile('/etc/zones/' + uuid + '-receiving.json', 9471 function (err, data) { 9472 var payload; 9473 9474 if (err) { 9475 callback(err); 9476 return; 9477 } 9478 9479 try { 9480 payload = JSON.parse(data.toString()); 9481 } catch (e) { 9482 callback(e); 9483 return; 9484 } 9485 9486 // installZone takes a payload 9487 installZone(payload, log, callback); 9488 } 9489 ); 9490 9491 }; 9492 9493 function getAllDatasets(vmobj) 9494 { 9495 var datasets = []; 9496 var disk; 9497 9498 if (vmobj.hasOwnProperty('zfs_filesystem')) { 9499 datasets.push(vmobj.zfs_filesystem); 9500 } 9501 9502 for (disk in vmobj.disks) { 9503 disk = vmobj.disks[disk]; 9504 if (disk.hasOwnProperty('zfs_filesystem')) { 9505 datasets.push(disk.zfs_filesystem); 9506 } 9507 } 9508 9509 return datasets; 9510 } 9511 9512 // 9513 // Headers are 512 bytes and look like: 9514 // 9515 // MAGIC-VMBUNDLE\0 9516 // <VERSION>\0 -- ASCII #s 9517 // <CHECKSUM>\0 -- ASCII (not yet used) 9518 // <OBJ-NAME>\0 -- max length: 256 9519 // <OBJ-SIZE>\0 -- ASCII # of bytes 9520 // <PADDED-SIZE>\0 -- ASCII # of bytes, must be multiple of 512 9521 // ...\0 9522 // 9523 function chunkHeader(name, size, padding) 9524 { 9525 var header = new Buffer(512); 9526 var pos = 0; 9527 9528 header.fill(0); 9529 pos += addString(header, 'MAGIC-VMBUNDLE', pos); 9530 pos += addString(header, sprintf('%d', 1), pos); 9531 pos += addString(header, 'CHECKSUM', pos); 9532 pos += addString(header, name, pos); 9533 pos += addString(header, sprintf('%d', size), pos); 9534 pos += addString(header, sprintf('%d', size + padding), pos); 9535 9536 return (header); 9537 } 9538 9539 // add the string to buffer at pos, returning pos of new end of the buffer. 9540 function addString(buf, str, pos) 9541 { 9542 var len = str.length; 9543 buf.write(str, pos); 9544 return (len + 1); 9545 } 9546 9547 function sendJSON(target, json, log, cb) 9548 { 9549 var header; 9550 var pad; 9551 var padding = 0; 9552 9553 assert(log, 'no logger passed for sendJSON()'); 9554 9555 if (target === 'stdout') { 9556 if ((json.length % 512) != 0) { 9557 padding = 512 - (json.length % 512); 9558 } 9559 header = chunkHeader('JSON', json.length, padding); 9560 process.stdout.write(header); 9561 process.stdout.write(json, 'ascii'); 9562 if (padding > 0) { 9563 pad = new Buffer(padding); 9564 pad.fill(0); 9565 process.stdout.write(pad); 9566 } 9567 cb(); 9568 } else { 9569 log.error('Don\'t know how to send JSON to ' 9570 + JSON.stringify(target)); 9571 cb(new Error('Don\'t know how to send JSON to ' 9572 + JSON.stringify(target))); 9573 } 9574 } 9575 9576 function sendDataset(target, dataset, log, callback) 9577 { 9578 var header; 9579 9580 assert(log, 'no logger passed for sendDataset()'); 9581 9582 if (target === 'stdout') { 9583 9584 async.series([ 9585 function (cb) { 9586 // delete any existing 'sending' snapshot 9587 zfs(['destroy', '-F', dataset + '@sending'], log, 9588 function (err, fds) { 9589 // We don't expect this to succeed, since that means 9590 // something left an @sending around. Warn if succeeds. 9591 if (!err) { 9592 log.warn('Destroyed pre-existing ' + dataset 9593 + '@sending'); 9594 } 9595 cb(); 9596 } 9597 ); 9598 }, function (cb) { 9599 zfs(['snapshot', dataset + '@sending'], log, 9600 function (err, fds) { 9601 9602 cb(err); 9603 }); 9604 }, function (cb) { 9605 header = chunkHeader(dataset, 0, 0); 9606 process.stdout.write(header); 9607 cb(); 9608 }, function (cb) { 9609 var child; 9610 9611 child = spawn('/usr/sbin/zfs', 9612 ['send', '-p', dataset + '@sending'], 9613 {customFds: [-1, 1, -1]}); 9614 child.stderr.on('data', function (data) { 9615 var idx; 9616 var lines = trim(data.toString()).split('\n'); 9617 9618 for (idx in lines) { 9619 log.debug('zfs send: ' + trim(lines[idx])); 9620 } 9621 }); 9622 child.on('close', function (code) { 9623 log.debug('zfs send process exited with code ' 9624 + code); 9625 cb(); 9626 }); 9627 }, function (cb) { 9628 zfs(['destroy', '-F', dataset + '@sending'], log, 9629 function (err, fds) { 9630 if (err) { 9631 log.warn(err, 'Unable to destroy ' + dataset 9632 + '@sending: ' + err.message); 9633 } 9634 cb(err); 9635 } 9636 ); 9637 } 9638 ], function (err) { 9639 if (err) { 9640 log.error(err, 'Failed to send dataset: ' + err.message); 9641 } else { 9642 log.info('Successfully sent dataset'); 9643 } 9644 callback(err); 9645 }); 9646 } else { 9647 log.error('Don\'t know how to send datasets to ' 9648 + JSON.stringify(target)); 9649 callback(new Error('Don\'t know how to send datasets to ' 9650 + JSON.stringify(target))); 9651 } 9652 } 9653 9654 exports.send = function (uuid, target, options, callback) 9655 { 9656 var datasets; 9657 var log; 9658 var vmobj; 9659 9660 // options is optional 9661 if (arguments.length === 3) { 9662 callback = arguments[2]; 9663 options = {}; 9664 } 9665 9666 ensureLogging(true); 9667 if (options.hasOwnProperty('log')) { 9668 log = options.log; 9669 } else { 9670 log = VM.log.child({action: 'send', vm: uuid}); 9671 } 9672 9673 target = 'stdout'; 9674 9675 log.info('Sending VM ' + uuid + ' to: ' + JSON.stringify(target)); 9676 async.series([ 9677 function (cb) { 9678 // make sure we *can* send first, to avoid wasting cycles 9679 if (target === 'stdout' && tty.isatty(1)) { 9680 log.error('Cannot send VM to a TTY.'); 9681 cb(new Error('Cannot send VM to a TTY.')); 9682 } else { 9683 cb(); 9684 } 9685 }, function (cb) { 9686 // NOTE: for this load we always load all fields, because we need 9687 // to send them all to the target machine. 9688 VM.load(uuid, {log: log}, function (err, obj) { 9689 if (err) { 9690 cb(err); 9691 } else { 9692 vmobj = obj; 9693 cb(); 9694 } 9695 }); 9696 }, function (cb) { 9697 datasets = getAllDatasets(vmobj); 9698 if (datasets.length < 1) { 9699 log.error('Cannot send VM with no datasets.'); 9700 cb(new Error('VM has no datasets.')); 9701 } else { 9702 cb(); 9703 } 9704 }, function (cb) { 9705 if (vmobj.state !== 'stopped') { 9706 // In this case we need to stop it and make sure it stopped. 9707 VM.stop(uuid, {log: log}, function (e) { 9708 if (e) { 9709 log.error(e, 'unable to stop VM ' + uuid + ': ' 9710 + e.message); 9711 cb(e); 9712 return; 9713 } 9714 VM.load(uuid, {fields: ['zone_state', 'uuid'], log: log}, 9715 function (error, obj) { 9716 9717 if (error) { 9718 log.error(error, 'unable to reload VM ' + uuid 9719 + ': ' + error.message); 9720 return; 9721 } 9722 if (obj.zone_state !== 'installed') { 9723 log.error('after stop attempt, state is ' 9724 + obj.zone_state + ' != installed'); 9725 cb(new Error('state after stopping is ' 9726 + obj.zone_state + ' != installed')); 9727 return; 9728 } 9729 cb(); 9730 }); 9731 }); 9732 } else { 9733 // already stopped, good to go! 9734 cb(); 9735 } 9736 }, function (cb) { 9737 // Clean up trash left from broken datasets (see OS-388) 9738 try { 9739 fs.unlinkSync(vmobj.zonepath + '/SUNWattached.xml'); 9740 } catch (err) { 9741 // DO NOTHING, this file shouldn't have existed anyway. 9742 } 9743 try { 9744 fs.unlinkSync(vmobj.zonepath + '/SUNWdetached.xml'); 9745 } catch (err) { 9746 // DO NOTHING, this file shouldn't have existed anyway. 9747 } 9748 cb(); 9749 }, function (cb) { 9750 // send JSON 9751 var json = JSON.stringify(vmobj, null, 2) + '\n'; 9752 sendJSON(target, json, log, cb); 9753 }, function (cb) { 9754 // send datasets 9755 async.forEachSeries(datasets, function (ds, c) { 9756 sendDataset(target, ds, log, c); 9757 }, function (e) { 9758 if (e) { 9759 log.error('Failed to send datasets'); 9760 } 9761 cb(e); 9762 }); 9763 } 9764 ], function (err) { 9765 callback(err); 9766 }); 9767 }; 9768 9769 exports.create = function (payload, options, callback) 9770 { 9771 var log; 9772 9773 // options is optional 9774 if (arguments.length === 2) { 9775 callback = arguments[1]; 9776 options = {}; 9777 } 9778 9779 ensureLogging(true); 9780 9781 if (options.hasOwnProperty('log')) { 9782 log = options.log; 9783 } else { 9784 // default to VM.log until we have a uuid, then we'll switch. 9785 log = VM.log; 9786 } 9787 9788 log.info('Creating VM, original payload:\n' 9789 + JSON.stringify(payload, null, 2)); 9790 9791 async.waterfall([ 9792 function (cb) { 9793 // We get a UUID first so that we can attach as many log messages 9794 // as possible to this uuid. Since we don't have a UUID here, we 9795 // send VM.log as the logger. We'll switch to a log.child as soon 9796 // as we have uuid. 9797 createZoneUUID(payload, log, function (e, uuid) { 9798 // either payload will have .uuid or we'll return error here. 9799 cb(e); 9800 }); 9801 }, function (cb) { 9802 // If we got here, payload now has .uuid and we can start logging 9803 // messages with that uuid if we didn't already have a logger. 9804 if (!options.hasOwnProperty('log')) { 9805 log = VM.log.child({action: 'create', vm: payload.uuid}); 9806 } 9807 cb(); 9808 }, function (cb) { 9809 normalizePayload(payload, null, log, function (err) { 9810 if (err) { 9811 log.error(err, 'Failed to validate payload: ' 9812 + err.message); 9813 } else { 9814 log.debug('normalized payload:\n' 9815 + JSON.stringify(payload, null, 2)); 9816 } 9817 cb(err); 9818 }); 9819 }, function (cb) { 9820 checkDatasetProvisionable(payload, log, function (provisionable) { 9821 if (!provisionable) { 9822 log.error('checkDatasetProvisionable() says dataset is ' 9823 + 'unprovisionable'); 9824 cb(new Error('provisioning dataset ' + payload.image_uuid 9825 + ' with brand ' + payload.brand 9826 + ' is not supported')); 9827 return; 9828 } 9829 cb(); 9830 }); 9831 }, function (cb) { 9832 if (BRAND_OPTIONS[payload.brand].features.type === 'KVM') { 9833 createVM(payload, log, function (error, result) { 9834 if (error) { 9835 cb(error); 9836 } else { 9837 cb(null, {'uuid': payload.uuid, 9838 'zonename': payload.zonename}); 9839 } 9840 }); 9841 } else { 9842 createZone(payload, log, function (error, result) { 9843 if (error) { 9844 cb(error); 9845 } else { 9846 cb(null, {'uuid': payload.uuid, 9847 'zonename': payload.zonename}); 9848 } 9849 }); 9850 } 9851 } 9852 ], function (err, obj) { 9853 callback(err, obj); 9854 }); 9855 }; 9856 9857 // delete a zvol 9858 function deleteVolume(volume, log, callback) 9859 { 9860 var args; 9861 var origin; 9862 9863 assert(log, 'no logger passed to deleteVolume()'); 9864 9865 if (volume.missing) { 9866 // this volume doesn't actually exist, so skip trying to delete. 9867 log.info('volume ' + volume.path + ' doesn\'t exist, skipping ' 9868 + 'deletion'); 9869 callback(); 9870 return; 9871 } 9872 9873 async.series([ 9874 function (cb) { 9875 args = ['get', '-Ho', 'value', 'origin', volume.zfs_filesystem]; 9876 zfs(args, log, function (err, fds) { 9877 if (err && fds.stderr.match('dataset does not exist')) { 9878 log.info('volume ' + volume.path + ' doesn\'t exist, ' 9879 + 'skipping deletion'); 9880 cb(); 9881 } else { 9882 origin = trim(fds.stdout); 9883 log.info('found origin "' + origin + '"'); 9884 cb(err); 9885 } 9886 }); 9887 }, function (cb) { 9888 // use recursive delete to handle possible snapshots on volume 9889 args = ['destroy', '-rF', volume.zfs_filesystem]; 9890 zfs(args, log, function (err, fds) { 9891 // err will be non-null if something broke 9892 cb(err); 9893 }); 9894 }, function (cb) { 9895 // we never delete an @final snapshot, that's the one from recv 9896 // that imgadm left around for us on purpose. 9897 if (!origin || origin.length < 1 || origin == '-' 9898 || origin.match('@final')) { 9899 9900 cb(); 9901 return; 9902 } 9903 args = ['destroy', '-rF', origin]; 9904 zfs(args, log, function (err, fds) { 9905 // err will be non-null if something broke 9906 cb(err); 9907 }); 9908 } 9909 ], function (err) { 9910 callback(err); 9911 }); 9912 } 9913 9914 function deleteZone(uuid, log, callback) 9915 { 9916 var load_fields; 9917 var vmobj; 9918 9919 assert(log, 'no logger passed to deleteZone()'); 9920 9921 load_fields = [ 9922 'archive_on_delete', 9923 'disks', 9924 'uuid', 9925 'zonename' 9926 ]; 9927 9928 async.series([ 9929 function (cb) { 9930 VM.load(uuid, {fields: load_fields, log: log}, function (err, obj) { 9931 if (err) { 9932 cb(err); 9933 return; 9934 } 9935 vmobj = obj; 9936 cb(); 9937 }); 9938 }, function (cb) { 9939 log.debug('archive_on_delete is set to ' 9940 + !!vmobj.archive_on_delete); 9941 if (!vmobj.archive_on_delete) { 9942 cb(); 9943 return; 9944 } 9945 archiveVM(vmobj.uuid, log, function () { 9946 cb(); 9947 }); 9948 // TODO: replace these next two with VM.stop(..{force: true} ? 9949 }, function (cb) { 9950 log.debug('setting autoboot=false'); 9951 zonecfg(['-u', uuid, 'set autoboot=false'], log, function (e, fds) { 9952 if (e) { 9953 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9954 'Error setting autoboot=false'); 9955 } else { 9956 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 9957 'set autoboot=false'); 9958 } 9959 cb(); 9960 }); 9961 }, function (cb) { 9962 log.debug('halting zone'); 9963 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) { 9964 if (e) { 9965 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9966 'Error halting zone'); 9967 } else { 9968 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 9969 'halted zone'); 9970 } 9971 cb(); 9972 }); 9973 }, function (cb) { 9974 log.debug('uninstalling zone'); 9975 zoneadm(['-u', uuid, 'uninstall', '-F'], log, function (e, fds) { 9976 if (e) { 9977 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9978 'Error uninstalling zone: ' + e.message); 9979 } else { 9980 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 9981 'uninstalled zone'); 9982 } 9983 cb(); 9984 }); 9985 }, function (cb) { 9986 function loggedDeleteVolume(volume, callbk) { 9987 return deleteVolume(volume, log, callbk); 9988 } 9989 9990 if (vmobj && vmobj.hasOwnProperty('disks')) { 9991 async.forEachSeries(vmobj.disks, loggedDeleteVolume, 9992 function (err) { 9993 if (err) { 9994 log.error(err, 'Unknown error deleting volumes: ' 9995 + err.message); 9996 cb(err); 9997 } else { 9998 log.info('successfully deleted volumes'); 9999 cb(); 10000 } 10001 } 10002 ); 10003 } else { 10004 log.debug('skipping volume destruction for diskless ' 10005 + vmobj.uuid); 10006 cb(); 10007 } 10008 }, function (cb) { 10009 if (vmobj.zonename) { 10010 log.debug('deleting zone'); 10011 // XXX for some reason -u <uuid> doesn't work with delete 10012 zonecfg(['-z', vmobj.zonename, 'delete', '-F'], log, 10013 function (e, fds) { 10014 10015 if (e) { 10016 log.warn({err: e, stdout: fds.stdout, 10017 stderr: fds.stderr}, 'Error deleting VM'); 10018 } else { 10019 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 10020 'deleted VM ' + uuid); 10021 } 10022 cb(); 10023 }); 10024 } else { 10025 cb(); 10026 } 10027 }, function (cb) { 10028 VM.load(uuid, {fields: ['uuid'], log: log, missing_ok: true}, 10029 function (err, obj) { 10030 10031 var gone = /^zoneadm:.*: No such zone configured/; 10032 if (err && err.message.match(gone)) { 10033 // the zone is gone, that's good. 10034 log.debug('confirmed VM is gone.'); 10035 cb(); 10036 } else if (err) { 10037 // there was a non-expected error. 10038 cb(err); 10039 } else { 10040 // the VM still exists! 10041 err = new Error('VM still exists after delete.'); 10042 err.code = 'EEXIST'; 10043 cb(err); 10044 } 10045 }); 10046 }, function (cb) { 10047 // delete the incoming payload if it exists 10048 fs.unlink('/etc/zones/' + vmobj.uuid + '-receiving.json', 10049 function (e) { 10050 // we can't do anyhing if this fails other than log 10051 if (e && e.code !== 'ENOENT') { 10052 log.warn(e, 'Failed to delete ' + vmobj.uuid 10053 + '-receiving.json (' + e.code + '): ' + e.message); 10054 } 10055 cb(); 10056 } 10057 ); 10058 } 10059 ], function (error) { 10060 callback(error); 10061 }); 10062 } 10063 10064 exports.delete = function (uuid, options, callback) 10065 { 10066 var attemptDelete; 10067 var last_try = 16; 10068 var log; 10069 var next_try = 1; 10070 var tries = 0; 10071 10072 // options is optional 10073 if (arguments.length === 2) { 10074 callback = arguments[1]; 10075 options = {}; 10076 } 10077 10078 ensureLogging(true); 10079 10080 if (options.hasOwnProperty('log')) { 10081 log = options.log; 10082 } else { 10083 log = VM.log.child({action: 'delete', vm: uuid}); 10084 } 10085 10086 log.info('Deleting VM ' + uuid); 10087 10088 attemptDelete = function (cb) { 10089 next_try = (next_try * 2); 10090 deleteZone(uuid, log, function (err) { 10091 tries++; 10092 if (err && err.code === 'EEXIST') { 10093 // zone still existed, try again if we've not tried too much. 10094 if (next_try <= last_try) { 10095 log.info('VM.delete(' + tries + '): still there, ' 10096 + 'will try again in: ' + next_try + ' secs'); 10097 setTimeout(function () { 10098 // try again 10099 attemptDelete(cb); 10100 }, next_try * 1000); 10101 } else { 10102 log.warn('VM.delete(' + tries + '): still there after' 10103 + ' ' + next_try + ' seconds, giving up.'); 10104 cb(new Error('delete failed after ' + tries + ' attempts. ' 10105 + '(check the log for details)')); 10106 return; 10107 } 10108 } else if (err) { 10109 // error but not one we can retry from. 10110 log.error(err, 'VM.delete: FATAL: ' + err.message); 10111 cb(err); 10112 } else { 10113 // success! 10114 log.debug('VM.delete: SUCCESS'); 10115 cb(); 10116 } 10117 }); 10118 }; 10119 10120 attemptDelete(function (err) { 10121 if (err) { 10122 log.error(err); 10123 } 10124 callback(err); 10125 }); 10126 }; 10127 10128 // This function needs vmobj to have: 10129 // 10130 // brand 10131 // never_booted 10132 // uuid 10133 // zonename 10134 // 10135 function startZone(vmobj, log, callback) 10136 { 10137 var set_autoboot = 'set autoboot=true'; 10138 var uuid = vmobj.uuid; 10139 10140 assert(log, 'no logger passed to startZone()'); 10141 10142 log.debug('startZone starting ' + uuid); 10143 10144 // 10145 // We set autoboot (or vm-autoboot) here because we've just intentionally 10146 // started this vm, so we want it to come up if the host is rebooted. 10147 // 10148 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 10149 set_autoboot = 'select attr name=vm-autoboot; set value=true; end'; 10150 } 10151 10152 async.series([ 10153 function (cb) { 10154 // do the booting 10155 zoneadm(['-u', uuid, 'boot', '-X'], log, function (err, boot_fds) { 10156 if (err) { 10157 log.error({err: err, stdout: boot_fds.stdout, 10158 stderr: boot_fds.stderr}, 'zoneadm failed to boot ' 10159 + 'VM'); 10160 } else { 10161 log.debug({stdout: boot_fds.stdout, 10162 stderr: boot_fds.stderr}, 'zoneadm booted VM'); 10163 } 10164 cb(err); 10165 }); 10166 }, function (cb) { 10167 // ensure it booted 10168 VM.waitForZoneState(vmobj, 'running', {timeout: 30, log: log}, 10169 function (err, result) { 10170 10171 if (err) { 10172 if (err.code === 'ETIMEOUT') { 10173 log.info(err, 'timeout waiting for zone to go to ' 10174 + '"running"'); 10175 } else { 10176 log.error(err, 'unknown error waiting for zone to go' 10177 + ' "running"'); 10178 } 10179 } else { 10180 // zone got to running 10181 log.info('VM seems to have switched to "running"'); 10182 } 10183 cb(err); 10184 }); 10185 }, function (cb) { 10186 zonecfg(['-u', uuid, set_autoboot], log, 10187 function (err, autoboot_fds) { 10188 10189 if (err) { 10190 // The vm is running at this point, erroring out here would 10191 // do no good, so we just log it. 10192 log.error({err: err, stdout: autoboot_fds.stdout, 10193 stderr: autoboot_fds.stderr}, 'startZone(): Failed to ' 10194 + set_autoboot + ' for ' + uuid); 10195 } else { 10196 log.debug({stdout: autoboot_fds.stdout, 10197 stderr: autoboot_fds.stderr}, 'set autoboot'); 10198 } 10199 cb(err); 10200 }); 10201 }, function (cb) { 10202 if (!vmobj.never_booted) { 10203 cb(); 10204 return; 10205 } 10206 zonecfg(['-u', uuid, 'remove attr name=never-booted' ], log, 10207 function (err, neverbooted_fds) { 10208 // Ignore errors here, because we're started. 10209 if (err) { 10210 log.warn({err: err, stdout: neverbooted_fds.stdout, 10211 stderr: neverbooted_fds.stderr}, 'failed to remove ' 10212 + 'never-booted flag'); 10213 } else { 10214 log.debug({stdout: neverbooted_fds.stdout, 10215 stderr: neverbooted_fds.stderr}, 'removed ' 10216 + 'never-booted flag'); 10217 } 10218 cb(); 10219 } 10220 ); 10221 } 10222 ], function (err) { 10223 if (!err) { 10224 log.info('Started ' + uuid); 10225 } 10226 callback(err); 10227 }); 10228 } 10229 10230 // build the qemu cmdline and start up a VM 10231 // 10232 // vmobj needs any of the following that are defined: 10233 // 10234 // boot 10235 // brand 10236 // cpu_type 10237 // default_gateway 10238 // disks 10239 // hostname 10240 // internal_metadata 10241 // never_booted 10242 // nics 10243 // qemu_extra_opts 10244 // qemu_opts 10245 // ram 10246 // resolvers 10247 // spice_opts 10248 // spice_password 10249 // spice_port 10250 // state 10251 // uuid 10252 // vcpus 10253 // vga 10254 // virtio_txtimer 10255 // virtio_txburst 10256 // vnc_password 10257 // zone_state 10258 // zonename 10259 // zonepath 10260 // 10261 function startVM(vmobj, extra, log, callback) 10262 { 10263 var check_path; 10264 var cmdargs = []; 10265 var d; 10266 var defaultgw = ''; 10267 var disk; 10268 var diskargs = ''; 10269 var disk_idx = 0; 10270 var found; 10271 var hostname = vmobj.uuid; 10272 var mdata; 10273 var nic; 10274 var nic_idx = 0; 10275 var primary_found = false; 10276 var qemu_opts = ''; 10277 var r; 10278 var script; 10279 var spiceargs; 10280 var uuid = vmobj.uuid; 10281 var virtio_txburst; 10282 var virtio_txtimer; 10283 var vnic_opts; 10284 var zoneroot; 10285 10286 assert(log, 'no logger passed to startVM'); 10287 assert(vmobj.hasOwnProperty('zonepath'), 'missing zonepath'); 10288 10289 log.debug('startVM(' + uuid + ')'); 10290 10291 if (!vmobj.hasOwnProperty('state')) { 10292 callback(new Error('Cannot start VM ' + uuid + ' which has no state')); 10293 return; 10294 } 10295 10296 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning') 10297 || (vmobj.state === 'provisioning' 10298 && vmobj.zone_state !== 'installed')) { 10299 10300 callback(new Error('Cannot start VM from state: ' + vmobj.state 10301 + ', must be "stopped"')); 10302 return; 10303 } 10304 10305 zoneroot = path.join(vmobj.zonepath, '/root'); 10306 10307 // We're going to write to /startvm and /tmp/vm.metadata, we don't care if 10308 // they already exist, but we don't want them to be symlinks. 10309 try { 10310 assertSafeZonePath(zoneroot, '/startvm', 10311 {type: 'file', enoent_ok: true}); 10312 assertSafeZonePath(zoneroot, '/tmp/vm.metadata', 10313 {type: 'file', enoent_ok: true}); 10314 } catch (e) { 10315 log.error(e, 'Error validating files for startVM(): ' 10316 + e.message); 10317 callback(e); 10318 return; 10319 } 10320 10321 // XXX TODO: validate vmobj data is ok to start 10322 10323 cmdargs.push('-m', vmobj.ram); 10324 cmdargs.push('-name', vmobj.uuid); 10325 cmdargs.push('-uuid', vmobj.uuid); 10326 10327 if (vmobj.hasOwnProperty('cpu_type')) { 10328 cmdargs.push('-cpu', vmobj.cpu_type); 10329 } else { 10330 cmdargs.push('-cpu', 'qemu64'); 10331 } 10332 10333 if (vmobj.vcpus > 1) { 10334 cmdargs.push('-smp', vmobj.vcpus); 10335 } 10336 10337 for (disk in vmobj.disks) { 10338 if (vmobj.disks.hasOwnProperty(disk)) { 10339 disk = vmobj.disks[disk]; 10340 if (!disk.media) { 10341 disk.media = 'disk'; 10342 } 10343 diskargs = 'file=' + disk.path + ',if=' + disk.model 10344 + ',index=' + disk_idx + ',media=' + disk.media; 10345 if (disk.boot) { 10346 diskargs = diskargs + ',boot=on'; 10347 } 10348 cmdargs.push('-drive', diskargs); 10349 disk_idx++; 10350 } 10351 } 10352 10353 // extra payload can include additional disks that we want to include only 10354 // on this one boot. It can also contain a boot parameter to control boot 10355 // device. See qemu http://qemu.weilnetz.de/qemu-doc.html for info on 10356 // -boot options. 10357 if (extra.hasOwnProperty('disks')) { 10358 for (disk in extra.disks) { 10359 if (extra.disks.hasOwnProperty(disk)) { 10360 disk = extra.disks[disk]; 10361 10362 // ensure this is either a disk that gets mounted in or a 10363 // file that's been dropped in to the zonepath 10364 found = false; 10365 for (d in vmobj.disks) { 10366 if (!found && vmobj.disks.hasOwnProperty(d)) { 10367 d = vmobj.disks[d]; 10368 if (d.path === disk.path) { 10369 found = true; 10370 } 10371 } 10372 } 10373 check_path = path.join(vmobj.zonepath, 'root', disk.path); 10374 if (!found && fs.existsSync(check_path)) { 10375 found = true; 10376 } 10377 if (!found) { 10378 callback(new Error('Cannot find disk: ' + disk.path)); 10379 return; 10380 } 10381 10382 if (!disk.media) { 10383 disk.media = 'disk'; 10384 } 10385 diskargs = 'file=' + disk.path + ',if=' + disk.model 10386 + ',index=' + disk_idx + ',media=' + disk.media; 10387 if (disk.boot) { 10388 diskargs = diskargs + ',boot=on'; 10389 } 10390 cmdargs.push('-drive', diskargs); 10391 disk_idx++; 10392 } 10393 } 10394 } 10395 10396 // helpful values: 10397 // order=nc (network boot, then fallback to disk) 10398 // once=d (boot on disk once and the fallback to default) 10399 // order=c,once=d (boot on CDROM this time, but not subsequent boots) 10400 if (extra.hasOwnProperty('boot')) { 10401 cmdargs.push('-boot', extra.boot); 10402 } else if (vmobj.hasOwnProperty('boot')) { 10403 cmdargs.push('-boot', vmobj.boot); 10404 } else { 10405 // order=cd means try harddisk first (c) and cdrom if that fails (d) 10406 cmdargs.push('-boot', 'order=cd'); 10407 } 10408 10409 if (vmobj.hasOwnProperty('hostname')) { 10410 hostname = vmobj.hostname; 10411 } 10412 10413 if (vmobj.hasOwnProperty('default_gateway')) { 10414 defaultgw = vmobj['default_gateway']; 10415 } 10416 10417 /* 10418 * These tunables are set for all virtio vnics on this VM. 10419 */ 10420 virtio_txtimer = VIRTIO_TXTIMER_DEFAULT; 10421 virtio_txburst = VIRTIO_TXBURST_DEFAULT; 10422 if (vmobj.hasOwnProperty('virtio_txtimer')) { 10423 virtio_txtimer = vmobj.virtio_txtimer; 10424 } 10425 if (vmobj.hasOwnProperty('virtio_txburst')) { 10426 virtio_txburst = vmobj.virtio_txburst; 10427 } 10428 10429 for (nic in vmobj.nics) { 10430 if (vmobj.nics.hasOwnProperty(nic)) { 10431 nic = vmobj.nics[nic]; 10432 10433 // for virtio devices, we want to be able to set the txtimer and 10434 // txburst so we use a '-device' instead of a '-net' line. 10435 if (nic.model === 'virtio') { 10436 cmdargs.push('-device', 10437 'virtio-net-pci,mac=' + nic.mac 10438 + ',tx=timer,x-txtimer=' + virtio_txtimer 10439 + ',x-txburst=' + virtio_txburst 10440 + ',vlan=' + nic_idx); 10441 } else { 10442 cmdargs.push('-net', 10443 'nic,macaddr=' + nic.mac 10444 + ',vlan=' + nic_idx 10445 + ',name=' + nic.interface 10446 + ',model=' + nic.model); 10447 } 10448 vnic_opts = 'vnic,name=' + nic.interface 10449 + ',vlan=' + nic_idx 10450 + ',ifname=' + nic.interface; 10451 10452 if (nic.ip != 'dhcp') { 10453 vnic_opts = vnic_opts 10454 + ',ip=' + nic.ip 10455 + ',netmask=' + nic.netmask; 10456 } 10457 10458 // The primary network provides the resolvers, default gateway 10459 // and hostname to prevent vm from trying to use settings 10460 // from more than one nic 10461 if (!primary_found) { 10462 if (nic.hasOwnProperty('primary') && nic.primary) { 10463 if (nic.hasOwnProperty('gateway') && nic.ip != 'dhcp') { 10464 vnic_opts += ',gateway_ip=' + nic.gateway; 10465 } 10466 primary_found = true; 10467 } else if (defaultgw && nic.hasOwnProperty('gateway') 10468 && nic.gateway == defaultgw) { 10469 10470 /* 10471 * XXX this exists here for backward compatibilty. New VMs 10472 * and old VMs that are upgraded should not use 10473 * default_gateway. When we've implemented autoupgrade 10474 * this block (and all reference to default_gateway) 10475 * can be removed. 10476 */ 10477 10478 if (nic.ip != 'dhcp') { 10479 vnic_opts += ',gateway_ip=' + nic.gateway; 10480 } 10481 primary_found = true; 10482 } 10483 10484 if (primary_found && nic.ip != 'dhcp') { 10485 if (hostname) { 10486 vnic_opts += ',hostname=' + hostname; 10487 } 10488 if (vmobj.hasOwnProperty('resolvers')) { 10489 for (r in vmobj.resolvers) { 10490 vnic_opts += ',dns_ip' + r + '=' 10491 + vmobj.resolvers[r]; 10492 } 10493 } 10494 } 10495 } 10496 10497 cmdargs.push('-net', vnic_opts); 10498 nic_idx++; 10499 } 10500 } 10501 10502 cmdargs.push('-smbios', 'type=1,manufacturer=Joyent,' 10503 + 'product=SmartDC HVM,version=6.2012Q1,' 10504 + 'serial=' + vmobj.uuid + ',uuid=' + vmobj.uuid + ',' 10505 + 'sku=001,family=Virtual Machine'); 10506 10507 cmdargs.push('-pidfile', '/tmp/vm.pid'); 10508 10509 if (vmobj.hasOwnProperty('vga')) { 10510 cmdargs.push('-vga', vmobj.vga); 10511 } else { 10512 cmdargs.push('-vga', 'std'); 10513 } 10514 10515 cmdargs.push('-chardev', 10516 'socket,id=qmp,path=/tmp/vm.qmp,server,nowait'); 10517 cmdargs.push('-qmp', 'chardev:qmp'); 10518 10519 // serial0 is for serial console 10520 cmdargs.push('-chardev', 10521 'socket,id=serial0,path=/tmp/vm.console,server,nowait'); 10522 cmdargs.push('-serial', 'chardev:serial0'); 10523 10524 // serial1 is used for metadata API 10525 cmdargs.push('-chardev', 10526 'socket,id=serial1,path=/tmp/vm.ttyb,server,nowait'); 10527 cmdargs.push('-serial', 'chardev:serial1'); 10528 10529 if (!vmobj.qemu_opts) { 10530 if (vmobj.hasOwnProperty('vnc_password') 10531 && vmobj.vnc_password.length > 0) { 10532 10533 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc,password'); 10534 } else { 10535 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc'); 10536 } 10537 if (vmobj.hasOwnProperty('spice_port') 10538 && vmobj.spice_port !== -1) { 10539 10540 spiceargs = 'sock=/tmp/vm.spice'; 10541 if (!vmobj.hasOwnProperty('spice_password') 10542 || vmobj.spice_password.length <= 0) { 10543 10544 spiceargs = spiceargs + ',disable-ticketing'; 10545 10546 // Otherwise, spice password is set via qmp, so we don't 10547 // need to do anything here 10548 } 10549 if (vmobj.hasOwnProperty('spice_opts') 10550 && vmobj.spice_opts.length > 0) { 10551 10552 spiceargs = spiceargs + ',' + vmobj.spice_opts; 10553 } 10554 cmdargs.push('-spice', spiceargs); 10555 } 10556 cmdargs.push('-parallel', 'none'); 10557 cmdargs.push('-usb'); 10558 cmdargs.push('-usbdevice', 'tablet'); 10559 cmdargs.push('-k', 'en-us'); 10560 } else { 10561 qemu_opts = vmobj.qemu_opts.toString(); 10562 } 10563 10564 if (vmobj.qemu_extra_opts) { 10565 qemu_opts = qemu_opts + ' ' + vmobj.qemu_extra_opts; 10566 } 10567 10568 // This actually creates the qemu process 10569 script = '#!/usr/bin/bash\n\n' 10570 + 'exec >/tmp/vm.startvm.log 2>&1\n\n' 10571 + 'set -o xtrace\n\n' 10572 + 'if [[ -x /startvm.zone ]]; then\n' 10573 + ' exec /smartdc/bin/qemu-exec /startvm.zone "' 10574 + cmdargs.join('" "') 10575 + '" ' + qemu_opts + '\n' 10576 + 'else\n' 10577 + ' exec /smartdc/bin/qemu-exec /smartdc/bin/qemu-system-x86_64 "' 10578 + cmdargs.join('" "') 10579 + '" ' + qemu_opts + '\n' 10580 + 'fi\n\n' 10581 + 'exit 1\n'; 10582 10583 try { 10584 fs.writeFileSync(vmobj.zonepath + '/root/startvm', script); 10585 fs.chmodSync(vmobj.zonepath + '/root/startvm', '0755'); 10586 } catch (e) { 10587 log.warn(e, 'Unable to create /startvm script in ' + vmobj.uuid); 10588 callback(new Error('cannot create /startvm')); 10589 return; 10590 } 10591 10592 mdata = { 10593 'internal_metadata': 10594 vmobj.internal_metadata ? vmobj.internal_metadata : {} 10595 }; 10596 fs.writeFile(path.join(vmobj.zonepath, '/root/tmp/vm.metadata'), 10597 JSON.stringify(mdata, null, 2) + '\n', 10598 function (err) { 10599 if (err) { 10600 log.debug(err, 'FAILED TO write metadata to ' 10601 + '/tmp/vm.metadata: ' + err.message); 10602 callback(err); 10603 } else { 10604 log.debug('wrote metadata to /tmp/vm.metadata'); 10605 startZone(vmobj, log, callback); 10606 } 10607 } 10608 ); 10609 } 10610 10611 // according to usr/src/common/zfs/zfs_namecheck.c allowed characters are: 10612 // 10613 // alphanumeric characters plus the following: [-_.:%] 10614 // 10615 function validSnapshotName(snapname, log) 10616 { 10617 assert(log, 'no logger passed to validSnapshotName()'); 10618 10619 if (snapname.length < 1 || snapname.length > MAX_SNAPNAME_LENGTH) { 10620 log.error('Invalid snapname length: ' + snapname.length 10621 + ' valid range: [1-' + MAX_SNAPNAME_LENGTH + ']'); 10622 return (false); 10623 } 10624 10625 if (snapname.match(/[^a-zA-Z0-9\-\_\.\:\%]/)) { 10626 log.error('Invalid snapshot name: contains invalid characters.'); 10627 return (false); 10628 } 10629 10630 return (true); 10631 } 10632 10633 function performSnapshotRollback(snapshots, log, callback) 10634 { 10635 assert(log, 'no logger passed to performSnapshotRollback()'); 10636 10637 // NOTE: we assume machine is stopped and snapshots are already validated 10638 10639 function rollback(snapname, cb) { 10640 var args; 10641 10642 args = ['rollback', '-r', snapname]; 10643 zfs(args, log, function (zfs_err, fds) { 10644 if (zfs_err) { 10645 log.error({'err': zfs_err, 'stdout': fds.stdout, 10646 'stderr': fds.stdout}, 'zfs rollback of ' + snapname 10647 + ' failed.'); 10648 cb(zfs_err); 10649 return; 10650 } 10651 log.info('rolled back snapshot ' + snapname); 10652 log.debug('zfs destroy stdout: ' + fds.stdout); 10653 log.debug('zfs destroy stderr: ' + fds.stderr); 10654 cb(); 10655 }); 10656 } 10657 10658 async.forEachSeries(snapshots, rollback, function (err) { 10659 if (err) { 10660 log.error(err, 'Unable to rollback some datasets.'); 10661 } 10662 callback(err); 10663 }); 10664 } 10665 10666 function updateZonecfgTimestamp(vmobj, callback) 10667 { 10668 var file; 10669 var now; 10670 10671 assert(vmobj.zonename, 'updateZonecfgTimestamp() vmobj must have ' 10672 + '.zonename'); 10673 10674 file = path.join('/etc/zones/', vmobj.zonename + '.xml'); 10675 now = new Date(); 10676 10677 fs.utimes(file, now, now, callback); 10678 } 10679 10680 exports.rollback_snapshot = function (uuid, snapname, options, callback) 10681 { 10682 var load_fields; 10683 var log; 10684 10685 // options is optional 10686 if (arguments.length === 3) { 10687 callback = arguments[2]; 10688 options = {}; 10689 } 10690 10691 ensureLogging(true); 10692 if (options.hasOwnProperty('log')) { 10693 log = options.log; 10694 } else { 10695 log = VM.log.child({action: 'rollback_snapshot', vm: uuid}); 10696 } 10697 10698 if (!validSnapshotName(snapname, log)) { 10699 callback(new Error('Invalid snapshot name')); 10700 return; 10701 } 10702 10703 load_fields = [ 10704 'brand', 10705 'snapshots', 10706 'zfs_filesystem', 10707 'state', 10708 'uuid' 10709 ]; 10710 10711 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10712 var found; 10713 var snap; 10714 var snapshot_list = []; 10715 10716 if (err) { 10717 callback(err); 10718 return; 10719 } 10720 10721 if (vmobj.brand === 'kvm') { 10722 callback(new Error('snapshots for KVM VMs currently unsupported')); 10723 return; 10724 } 10725 10726 found = false; 10727 if (vmobj.hasOwnProperty('snapshots')) { 10728 for (snap in vmobj.snapshots) { 10729 if (vmobj.snapshots[snap].name === snapname) { 10730 found = true; 10731 break; 10732 } 10733 } 10734 } 10735 if (!found) { 10736 callback(new Error('No snapshot named "' + snapname + '" for ' 10737 + uuid)); 10738 return; 10739 } 10740 10741 snapshot_list = [vmobj.zfs_filesystem + '@vmsnap-' + snapname]; 10742 10743 if (vmobj.state !== 'stopped') { 10744 VM.stop(vmobj.uuid, {'force': true, log: log}, function (stop_err) { 10745 if (stop_err) { 10746 log.error(stop_err, 'failed to stop VM ' + vmobj.uuid 10747 + ': ' + stop_err.message); 10748 callback(stop_err); 10749 return; 10750 } 10751 performSnapshotRollback(snapshot_list, log, 10752 function (rollback_err) { 10753 10754 if (rollback_err) { 10755 log.error(rollback_err, 'failed to ' 10756 + 'performSnapshotRollback'); 10757 callback(rollback_err); 10758 return; 10759 } 10760 if (options.do_not_start) { 10761 callback(); 10762 } else { 10763 VM.start(vmobj.uuid, {}, {log: log}, callback); 10764 } 10765 return; 10766 }); 10767 }); 10768 } else { 10769 performSnapshotRollback(snapshot_list, log, 10770 function (rollback_err) { 10771 10772 if (rollback_err) { 10773 log.error(rollback_err, 'failed to ' 10774 + 'performSnapshotRollback'); 10775 callback(rollback_err); 10776 return; 10777 } 10778 if (options.do_not_start) { 10779 callback(); 10780 } else { 10781 VM.start(vmobj.uuid, {}, {log: log}, callback); 10782 } 10783 return; 10784 }); 10785 } 10786 }); 10787 }; 10788 10789 exports.delete_snapshot = function (uuid, snapname, options, callback) 10790 { 10791 var load_fields; 10792 var log; 10793 10794 // options is optional 10795 if (arguments.length === 3) { 10796 callback = arguments[2]; 10797 options = {}; 10798 } 10799 10800 ensureLogging(true); 10801 if (options.hasOwnProperty('log')) { 10802 log = options.log; 10803 } else { 10804 log = VM.log.child({action: 'delete_snapshot', vm: uuid}); 10805 } 10806 10807 if (!validSnapshotName(snapname, log)) { 10808 callback(new Error('Invalid snapshot name')); 10809 return; 10810 } 10811 10812 load_fields = [ 10813 'brand', 10814 'snapshots', 10815 'zfs_filesystem', 10816 'zonepath', 10817 'zonename' 10818 ]; 10819 10820 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10821 var found; 10822 var mountpath; 10823 var mountpoint; 10824 var snap; 10825 var zoneroot; 10826 10827 if (err) { 10828 callback(err); 10829 return; 10830 } 10831 10832 if (vmobj.brand === 'kvm') { 10833 callback(new Error('snapshots for KVM VMs currently unsupported')); 10834 return; 10835 } 10836 10837 found = false; 10838 if (vmobj.hasOwnProperty('snapshots')) { 10839 for (snap in vmobj.snapshots) { 10840 if (vmobj.snapshots[snap].name === snapname) { 10841 found = true; 10842 break; 10843 } 10844 } 10845 } 10846 if (!found) { 10847 callback(new Error('No snapshot named "' + snapname + '" for ' 10848 + uuid)); 10849 return; 10850 } 10851 10852 zoneroot = vmobj.zonepath + '/root'; 10853 mountpath = '/checkpoints/' + snapname; 10854 mountpoint = zoneroot + '/' + mountpath; 10855 10856 async.waterfall([ 10857 function (cb) { 10858 // Ensure it's safe for us to be doing something in this dir 10859 try { 10860 assertSafeZonePath(zoneroot, mountpath, 10861 {type: 'dir', enoent_ok: true}); 10862 } catch (e) { 10863 log.error(e, 'Unsafe mountpoint for checkpoints: ' 10864 + e.message); 10865 cb(e); 10866 return; 10867 } 10868 cb(); 10869 }, function (cb) { 10870 // umount snapshot 10871 var argv; 10872 var cmd = '/usr/sbin/umount'; 10873 10874 argv = [mountpoint]; 10875 10876 execFile(cmd, argv, function (e, stdout, stderr) { 10877 if (e) { 10878 log.error({err: e}, 'There was an error while ' 10879 + 'unmounting the snapshot: ' + e.message); 10880 // we treat an error here as fatal only if the error 10881 // was something other than 'not mounted' 10882 if (!stderr.match(/ not mounted/)) { 10883 cb(e); 10884 return; 10885 } 10886 } else { 10887 log.trace('umounted ' + mountpoint); 10888 } 10889 cb(); 10890 }); 10891 }, function (cb) { 10892 // remove the mountpoint directory 10893 fs.rmdir(mountpoint, function (e) { 10894 if (e) { 10895 log.error(e); 10896 } else { 10897 log.trace('removed directory ' + mountpoint); 10898 } 10899 cb(); // XXX not fatal because might also not exist 10900 }); 10901 }, function (cb) { 10902 var args; 10903 10904 args = ['destroy', vmobj.zfs_filesystem + '@vmsnap-' 10905 + snapname]; 10906 10907 zfs(args, log, function (e, fds) { 10908 if (e) { 10909 log.error({'err': e, 'stdout': fds.stdout, 10910 'stderr': fds.stdout}, 'zfs destroy failed.'); 10911 cb(e); 10912 return; 10913 } 10914 log.debug({err: e, stdout: fds.stdout, stderr: fds.stderr}, 10915 'zfs destroy ' + vmobj.zfs_filesystem + '@vmsnap-' 10916 + snapname); 10917 cb(); 10918 }); 10919 }, function (cb) { 10920 updateZonecfgTimestamp(vmobj, function (e) { 10921 if (e) { 10922 log.warn(e, 'failed to update timestamp after deleting ' 10923 + 'snapshot'); 10924 } 10925 // don't pass err because there's no recovery possible 10926 // (the snapshot's gone) 10927 cb(); 10928 }); 10929 } 10930 ], function (error) { 10931 callback(error); 10932 }); 10933 }); 10934 }; 10935 10936 exports.create_snapshot = function (uuid, snapname, options, callback) 10937 { 10938 var load_fields; 10939 var log; 10940 10941 // options is optional 10942 if (arguments.length === 3) { 10943 callback = arguments[2]; 10944 options = {}; 10945 } 10946 10947 ensureLogging(true); 10948 10949 if (options.hasOwnProperty('log')) { 10950 log = options.log; 10951 } else { 10952 log = VM.log.child({action: 'create_snapshot', vm: uuid}); 10953 } 10954 10955 if (!validSnapshotName(snapname, log)) { 10956 callback(new Error('Invalid snapshot name')); 10957 return; 10958 } 10959 10960 load_fields = [ 10961 'brand', 10962 'datasets', 10963 'zone_state', 10964 'snapshots', 10965 'zfs_filesystem', 10966 'zonepath', 10967 'zonename' 10968 ]; 10969 10970 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10971 var full_snapname; 10972 var mountpath; 10973 var mountpoint; 10974 var mount_snapshot = true; 10975 var snap; 10976 var snapshot_list = []; 10977 var zoneroot; 10978 10979 if (err) { 10980 callback(err); 10981 return; 10982 } 10983 10984 if (vmobj.brand === 'kvm') { 10985 callback(new Error('snapshots for KVM VMs currently unsupported')); 10986 return; 10987 } 10988 10989 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 0) { 10990 callback(new Error('Cannot currently snapshot zones that have ' 10991 + 'datasets')); 10992 return; 10993 } 10994 10995 if (!vmobj.hasOwnProperty('zfs_filesystem')) { 10996 callback(new Error('vmobj missing zfs_filesystem, cannot create ' 10997 + 'snapshot')); 10998 return; 10999 } 11000 11001 full_snapname = vmobj.zfs_filesystem + '@vmsnap-' + snapname; 11002 11003 // Check that name not already used 11004 if (vmobj.hasOwnProperty('snapshots')) { 11005 for (snap in vmobj.snapshots) { 11006 snap = vmobj.snapshots[snap]; 11007 11008 if (snap.name === full_snapname) { 11009 callback(new Error('snapshot with name "' + snapname 11010 + '" already exists.')); 11011 return; 11012 } else { 11013 log.debug('SKIPPING ' + snap.name); 11014 } 11015 } 11016 } 11017 11018 snapshot_list.push(full_snapname); 11019 11020 // assert snapshot_list.length > 0 11021 11022 log.info('Taking snapshot "' + snapname + '" of ' + uuid); 11023 11024 zoneroot = vmobj.zonepath + '/root'; 11025 mountpath = '/checkpoints/' + snapname; 11026 mountpoint = zoneroot + '/' + mountpath; 11027 11028 async.waterfall([ 11029 function (cb) { 11030 // take the snapshot 11031 var args; 11032 args = ['snapshot'].concat(snapshot_list); 11033 11034 zfs(args, log, function (zfs_err, fds) { 11035 if (zfs_err) { 11036 log.error({err: zfs_err, stdout: fds.stdout, 11037 stderr: fds.stdout}, 'zfs snapshot failed.'); 11038 } else { 11039 log.debug({err: zfs_err, stdout: fds.stdout, 11040 stderr: fds.stderr}, 'zfs ' + args.join(' ')); 11041 } 11042 cb(zfs_err); 11043 }); 11044 }, function (cb) { 11045 11046 if (vmobj.zone_state !== 'running') { 11047 log.info('Not mounting snapshot as zone is in state ' 11048 + vmobj.zone_state + ', must be: running'); 11049 mount_snapshot = false; 11050 cb(); 11051 return; 11052 } 11053 11054 // Ensure it's safe for us to be doing something in this dir 11055 try { 11056 assertSafeZonePath(zoneroot, mountpath, 11057 {type: 'dir', enoent_ok: true}); 11058 } catch (e) { 11059 log.error(e, 'Unsafe mountpoint for checkpoints: ' 11060 + e.message); 11061 cb(e); 11062 return; 11063 } 11064 cb(); 11065 }, function (cb) { 11066 // Make the mountpoint directory and parent 11067 var newmode; 11068 11069 if (mount_snapshot === false) { 11070 cb(); 11071 return; 11072 } 11073 11074 /*jsl:ignore*/ 11075 newmode = 0755; 11076 /*jsl:end*/ 11077 11078 function doMkdir(dir, callbk) { 11079 fs.mkdir(dir, newmode, function (e) { 11080 if (e && e.code !== 'EEXIST') { 11081 log.error({err: e}, 'unable to create mountpoint ' 11082 + 'for checkpoints: ' + e.message); 11083 callbk(e); 11084 return; 11085 } 11086 callbk(); 11087 }); 11088 } 11089 11090 doMkdir(path.dirname(mountpoint), function (parent_e) { 11091 if (parent_e) { 11092 cb(parent_e); 11093 return; 11094 } 11095 doMkdir(mountpoint, function (dir_e) { 11096 if (dir_e) { 11097 cb(dir_e); 11098 return; 11099 } 11100 11101 log.debug('created ' + mountpoint); 11102 cb(); 11103 }); 11104 }); 11105 }, function (cb) { 11106 var argv; 11107 var cmd = '/usr/sbin/mount'; 11108 var snapdir; 11109 11110 if (mount_snapshot === false) { 11111 cb(); 11112 return; 11113 } 11114 11115 snapdir = vmobj.zonepath + '/.zfs/snapshot/vmsnap-' + snapname 11116 + '/root'; 11117 argv = [ '-F', 'lofs', '-o', 'ro,setuid,nodevices', snapdir, 11118 mountpoint]; 11119 11120 execFile(cmd, argv, function (e, stdout, stderr) { 11121 if (e) { 11122 log.error({err: e}, 'unable to mount snapshot: ' 11123 + e.message); 11124 } 11125 // not fatal becase snapshot was already created. 11126 cb(); 11127 }); 11128 }, function (cb) { 11129 // update timestamp so last_modified gets bumped 11130 updateZonecfgTimestamp(vmobj, function (e) { 11131 if (e) { 11132 log.warn(e, 11133 'failed to update timestamp after snapshot'); 11134 } 11135 // ignore error since there's no recovery 11136 // (snapshot was created) 11137 cb(); 11138 }); 11139 } 11140 ], function (error) { 11141 callback(error); 11142 }); 11143 }); 11144 }; 11145 11146 exports.start = function (uuid, extra, options, callback) 11147 { 11148 var load_fields; 11149 var log; 11150 11151 load_fields = [ 11152 'brand', 11153 'nics', 11154 'state', 11155 'uuid', 11156 'zone_state', 11157 'zonename', 11158 'zonepath' 11159 ]; 11160 11161 // options is optional 11162 if (arguments.length === 3) { 11163 callback = arguments[2]; 11164 options = {}; 11165 } 11166 11167 assert(callback, 'undefined callback!'); 11168 11169 ensureLogging(true); 11170 if (options.hasOwnProperty('log')) { 11171 log = options.log; 11172 } else { 11173 log = VM.log.child({action: 'start', vm: uuid}); 11174 } 11175 11176 log.info('Starting VM ' + uuid); 11177 11178 VM.load(uuid, {log: log, fields: load_fields}, function (err, vmobj) { 11179 if (err) { 11180 callback(err); 11181 } else { 11182 11183 if (vmobj.state === 'running') { 11184 err = new Error('VM ' + vmobj.uuid + ' is already ' 11185 + '\'running\''); 11186 err.code = 'EALREADYRUNNING'; 11187 callback(err); 11188 return; 11189 } 11190 11191 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning') 11192 || (vmobj.state === 'provisioning' 11193 && vmobj.zone_state !== 'installed')) { 11194 11195 err = new Error('Cannot to start vm from state "' + vmobj.state 11196 + '", must be "stopped".'); 11197 log.error(err); 11198 callback(err); 11199 return; 11200 } 11201 11202 lookupInvalidNicTags(vmobj.nics, log, function (e) { 11203 var kvm_load_fields = [ 11204 'boot', 11205 'brand', 11206 'cpu_type', 11207 'default_gateway', 11208 'disks', 11209 'hostname', 11210 'internal_metadata', 11211 'never_booted', 11212 'nics', 11213 'qemu_extra_opts', 11214 'qemu_opts', 11215 'ram', 11216 'resolvers', 11217 'spice_opts', 11218 'spice_password', 11219 'spice_port', 11220 'state', 11221 'uuid', 11222 'vcpus', 11223 'vga', 11224 'virtio_txtimer', 11225 'virtio_txburst', 11226 'vnc_password', 11227 'zone_state', 11228 'zonename', 11229 'zonepath' 11230 ]; 11231 11232 if (e) { 11233 callback(e); 11234 return; 11235 } 11236 11237 if (BRAND_OPTIONS[vmobj.brand].features.type === 'KVM') { 11238 // when we boot KVM we need a lot more fields, so load again 11239 // in that case to get the fields we need. 11240 VM.load(uuid, {log: log, fields: kvm_load_fields}, 11241 function (error, obj) { 11242 11243 if (error) { 11244 callback(error); 11245 return; 11246 } 11247 startVM(obj, extra, log, callback); 11248 }); 11249 } else if (BRAND_OPTIONS[vmobj.brand].features.type === 'OS') { 11250 startZone(vmobj, log, callback); 11251 } else { 11252 err = new Error('no idea how to start a vm with brand: ' 11253 + vmobj.brand); 11254 log.error(err); 11255 callback(err); 11256 } 11257 }); 11258 } 11259 }); 11260 }; 11261 11262 function setRctl(zonename, rctl, value, log, callback) 11263 { 11264 var args; 11265 11266 assert(log, 'no logger passed to setRctl()'); 11267 11268 args = ['-n', rctl, '-v', value.toString(), '-r', '-i', 'zone', zonename]; 11269 log.debug('/usr/bin/prctl ' + args.join(' ')); 11270 execFile('/usr/bin/prctl', args, function (error, stdout, stderr) { 11271 if (error) { 11272 log.error(error, 'setRctl() failed with: ' + stderr); 11273 callback(error); 11274 } else { 11275 callback(); 11276 } 11277 }); 11278 } 11279 11280 function resizeTmp(zonename, newsize, log, callback) 11281 { 11282 var args; 11283 11284 // NOTE: this used to update /etc/vfstab in the zone as well, but was 11285 // changed with OS-920. Now vfstab is updated by mdata-fetch in the 11286 // zone instead, so that will happen next boot. We still do the mount 11287 // so the property update happens on the running zone. 11288 11289 assert(log, 'no logger passed to resizeTmp()'); 11290 11291 args = [zonename, '/usr/sbin/mount', '-F', 'tmpfs', '-o', 'remount,size=' 11292 + newsize + 'm', '/tmp']; 11293 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 11294 execFile('/usr/sbin/zlogin', args, function (err, mnt_stdout, mnt_stderr) { 11295 if (err) { 11296 log.error({'err': err, 'stdout': mnt_stdout, 11297 'stderr': mnt_stderr}, 'zlogin for ' + zonename 11298 + ' exited with code ' + err.code + ' -- ' + err.message); 11299 // error here is not fatal as this should be fixed on reboot 11300 } 11301 11302 callback(); 11303 }); 11304 } 11305 11306 function resizeDisks(disks, updates, log, callback) 11307 { 11308 var d; 11309 var disk; 11310 var resized = 0; 11311 var vols = []; 11312 11313 assert(log, 'no logger passed to resizeDisks()'); 11314 11315 for (disk in updates) { 11316 disk = updates[disk]; 11317 for (d in disks) { 11318 d = disks[d]; 11319 if (d.path === disk.path && disk.hasOwnProperty('size')) { 11320 vols.push({'disk': d, 'new_size': disk.size}); 11321 } 11322 } 11323 } 11324 11325 function resize(vol, cb) { 11326 var args; 11327 var dsk = vol.disk; 11328 var size = vol.new_size; 11329 11330 if (dsk.hasOwnProperty('zfs_filesystem')) { 11331 if (dsk.size > size) { 11332 cb(new Error('cannot resize ' + dsk.zfs_filesystem 11333 + ' new size must be greater than current size. (' 11334 + dsk.size + ' > ' + dsk.size + ')')); 11335 } else if (dsk.size === size) { 11336 // no point resizing if the old+new are the same 11337 cb(); 11338 } else { 11339 args = ['set', 'volsize=' + size + 'M', dsk.zfs_filesystem]; 11340 zfs(args, log, function (err, fds) { 11341 resized++; 11342 cb(err); 11343 }); 11344 } 11345 } else { 11346 cb(new Error('could not find zfs_filesystem in ' 11347 + JSON.stringify(dsk))); 11348 } 11349 } 11350 11351 async.forEachSeries(vols, resize, function (err) { 11352 if (err) { 11353 log.error(err, 'Unable to resize disks'); 11354 callback(err); 11355 } else { 11356 callback(null, resized); 11357 } 11358 }); 11359 } 11360 11361 function updateVnicAllowedIPs(uuid, nic, log, callback) 11362 { 11363 var ips = []; 11364 11365 assert(log, 'no logger passed to updateVnicAllowedIPs()'); 11366 11367 if (!uuid || !nic.interface) { 11368 callback(); 11369 return; 11370 } 11371 11372 if (nic.hasOwnProperty('allow_ip_spoofing') && nic.allow_ip_spoofing) { 11373 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback); 11374 return; 11375 } 11376 11377 if (nic.hasOwnProperty('ip')) { 11378 ips.push(nic.ip); 11379 } 11380 11381 if (nic.hasOwnProperty('vrrp_primary_ip')) { 11382 ips.push(nic.vrrp_primary_ip); 11383 } 11384 11385 if (nic.hasOwnProperty('allowed_ips')) { 11386 ips = ips.concat(nic.allowed_ips); 11387 } 11388 11389 if (!ips.length === 0) { 11390 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback); 11391 } else { 11392 dladm.setLinkProp(uuid, nic.interface, 'allowed-ips', ips, log, 11393 callback); 11394 } 11395 } 11396 11397 function updateVnicProperties(uuid, vmobj, payload, log, callback) 11398 { 11399 assert(log, 'no logger passed to updateVnicProperties()'); 11400 11401 if (vmobj.state != 'running') { 11402 log.debug('VM not running: not updating vnic properties'); 11403 callback(null); 11404 return; 11405 } 11406 11407 if (!payload.hasOwnProperty('update_nics')) { 11408 log.debug( 11409 'No update_nics property: not updating vnic properties'); 11410 callback(null); 11411 return; 11412 } 11413 11414 async.forEach(payload.update_nics, function (nic, cb) { 11415 var opt; 11416 var needsUpdate = false; 11417 var needsIPupdate = false; 11418 var spoof_opts = { 11419 'allow_ip_spoofing': 'ip-nospoof', 11420 'allow_mac_spoofing': 'mac-nospoof', 11421 'allow_dhcp_spoofing': 'dhcp-nospoof', 11422 'allow_restricted_traffic': 'restricted' 11423 }; 11424 var vm_nic; 11425 11426 // First, determine if we've changed any of the spoofing opts in this 11427 // update: 11428 for (opt in spoof_opts) { 11429 if (nic.hasOwnProperty(opt)) { 11430 needsUpdate = true; 11431 break; 11432 } 11433 } 11434 11435 if (nic.hasOwnProperty('vrrp_primary_ip') 11436 || nic.hasOwnProperty('allowed_ips') 11437 || nic.hasOwnProperty('allow_ip_spoofing')) { 11438 needsIPupdate = true; 11439 } 11440 11441 for (vm_nic in vmobj.nics) { 11442 vm_nic = vmobj.nics[vm_nic]; 11443 if (vm_nic.mac == nic.mac) { 11444 break; 11445 } 11446 } 11447 11448 if (!vm_nic) { 11449 cb(new Error('Unknown NIC: ' + nic.mac)); 11450 return; 11451 } 11452 11453 if (!needsUpdate) { 11454 log.debug('No spoofing / allowed IP opts updated for nic "' 11455 + nic.mac + '": not updating'); 11456 if (needsIPupdate) { 11457 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11458 } else { 11459 cb(null); 11460 } 11461 return; 11462 } 11463 11464 // Using the updated nic object, figure out what spoofing opts to set 11465 for (opt in spoof_opts) { 11466 if (vm_nic.hasOwnProperty(opt) && fixBoolean(vm_nic[opt])) { 11467 delete spoof_opts[opt]; 11468 } 11469 } 11470 11471 if (vm_nic.hasOwnProperty('dhcp_server') 11472 && fixBoolean(vm_nic.dhcp_server)) { 11473 delete spoof_opts.allow_dhcp_spoofing; 11474 delete spoof_opts.allow_ip_spoofing; 11475 } 11476 11477 if (Object.keys(spoof_opts).length === 0) { 11478 dladm.resetLinkProp(uuid, vm_nic.interface, 'protection', log, 11479 function (err) { 11480 if (err) { 11481 cb(err); 11482 return; 11483 } 11484 if (needsIPupdate) { 11485 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11486 return; 11487 } 11488 cb(); 11489 return; 11490 }); 11491 } else { 11492 dladm.setLinkProp(uuid, vm_nic.interface, 'protection', 11493 Object.keys(spoof_opts).map(function (k) { 11494 return spoof_opts[k]; 11495 }), log, 11496 function (err) { 11497 if (err) { 11498 cb(err); 11499 return; 11500 } 11501 if (needsIPupdate) { 11502 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11503 return; 11504 } 11505 cb(); 11506 return; 11507 }); 11508 } 11509 }, function (err) { 11510 if (err) { 11511 callback(err); 11512 } else { 11513 callback(null); 11514 } 11515 }); 11516 } 11517 11518 // Run a fw.js function that requires all VM records 11519 function firewallVMrun(opts, fn, log, callback) 11520 { 11521 assert(log, 'no logger passed to firewallVMrun()'); 11522 VM.lookup({}, {fields: fw.VM_FIELDS, log: log}, function (err, records) { 11523 if (err) { 11524 callback(err); 11525 return; 11526 } 11527 11528 opts.vms = records; 11529 if (fn.name == 'validatePayload') { 11530 opts.logName = 'VM-create'; 11531 } else { 11532 opts.logName = 'VM-' + (fn.name || ''); 11533 } 11534 11535 if (opts.provisioning) { 11536 opts.vms.push(opts.provisioning); 11537 delete opts.provisioning; 11538 } 11539 11540 fn(opts, callback); 11541 return; 11542 }); 11543 } 11544 11545 function validateFirewall(payload, log, callback) 11546 { 11547 assert(log, 'no logger passed to validateFirewall()'); 11548 11549 log.debug(toValidate, 'Validating firewall payload'); 11550 var toValidate = payload.firewall; 11551 toValidate.provisioning = { 11552 'state': 'provisioning' 11553 }; 11554 11555 fw.VM_FIELDS.forEach(function (field) { 11556 if (payload.hasOwnProperty(field)) { 11557 toValidate.provisioning[field] = payload[field]; 11558 } 11559 }); 11560 11561 if (payload.hasOwnProperty('add_nics')) { 11562 toValidate.provisioning.nics = payload.add_nics; 11563 } 11564 11565 // We're not actually writing data to zonepath when validating, and we 11566 // don't actually have a zonepath created yet, so add a key so that the 11567 // payload passes validation 11568 if (!payload.hasOwnProperty('zonepath')) { 11569 toValidate.provisioning.zonepath = true; 11570 } 11571 11572 log.debug({ 11573 firewall: toValidate.firewall, 11574 provisioning: toValidate.provisioning, 11575 payload: payload 11576 }, 'Validating firewall payload'); 11577 11578 firewallVMrun(toValidate, fw.validatePayload, log, 11579 function (err, res) { 11580 if (err) { 11581 log.error(err, 'Error validating firewall payload'); 11582 err.message = 'Invalid firewall payload: ' + err.message; 11583 } 11584 11585 callback(err, res); 11586 return; 11587 }); 11588 } 11589 11590 function addFirewallData(payload, vmobj, log, callback) 11591 { 11592 var firewallOpts = payload.firewall; 11593 11594 assert(log, 'no logger passed to addFirewallData()'); 11595 11596 if (!payload.hasOwnProperty('firewall')) { 11597 firewallOpts = {}; 11598 } 11599 firewallOpts.localVMs = [vmobj]; 11600 11601 log.debug(firewallOpts, 'Adding firewall data'); 11602 firewallVMrun(firewallOpts, fw.add, log, function (err, res) { 11603 if (err) { 11604 log.error(err, 'Error adding firewall data'); 11605 } 11606 11607 callback(err, res); 11608 return; 11609 }); 11610 } 11611 11612 function updateFirewallData(payload, vmobj, log, callback) 11613 { 11614 var enablePrefix = 'En'; 11615 var enableFn = fw.enable; 11616 var firewallOpts = payload.firewall; 11617 11618 assert(log, 'no logger passed to updateFirewallData()'); 11619 11620 if (!payload.hasOwnProperty('firewall')) { 11621 firewallOpts = {}; 11622 } 11623 firewallOpts.localVMs = [vmobj]; 11624 11625 log.debug(firewallOpts, 'Updating firewall data'); 11626 firewallVMrun(firewallOpts, fw.update, log, function (err, res) { 11627 if (err) { 11628 log.error(err, 'Error updating firewall data'); 11629 } 11630 11631 if (!payload.hasOwnProperty('firewall_enabled')) { 11632 callback(err, res); 11633 return; 11634 } 11635 11636 if (!payload.firewall_enabled) { 11637 enableFn = fw.disable; 11638 enablePrefix = 'Dis'; 11639 } 11640 11641 log.debug('%sabling firewall for VM %s', enablePrefix, vmobj.uuid); 11642 firewallVMrun({ vm: vmobj }, enableFn, log, function (err2, res2) { 11643 if (err2) { 11644 log.error(err, 'Error %sabling firewall', 11645 enablePrefix.toLowerCase()); 11646 } 11647 11648 callback(err2, res2); 11649 return; 11650 }); 11651 }); 11652 } 11653 11654 function restartMetadataService(vmobj, payload, log, callback) { 11655 var args; 11656 11657 assert(log, 'no logger passed to restartMetadataService()'); 11658 11659 if (!BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 11660 || !BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 11661 || !BRAND_OPTIONS[vmobj.brand].features.mdata_restart) { 11662 log.debug('restarting mdata:fetch service not supported for brand ' 11663 + vmobj.brand); 11664 callback(); 11665 return; 11666 } 11667 11668 if (vmobj.state !== 'running' || !payload.hasOwnProperty('resolvers') 11669 && !payload.hasOwnProperty('routes') 11670 && !payload.hasOwnProperty('set_routes') 11671 && !payload.hasOwnProperty('remove_routes')) { 11672 callback(); 11673 return; 11674 } 11675 11676 log.debug('restarting metadata service for: ' + vmobj.uuid); 11677 11678 args = [vmobj.zonename, '/usr/sbin/svcadm', 'restart', 11679 'svc:/smartdc/mdata:fetch']; 11680 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 11681 execFile('/usr/sbin/zlogin', args, function (err, svc_stdout, svc_stderr) { 11682 if (err) { 11683 log.error({'err': err, 'stdout': svc_stdout, 11684 'stderr': svc_stderr}, 'zlogin for ' + vmobj.zonename 11685 + ' exited with code' + err.code + err.message); 11686 // error here is not fatal as this should be fixed on reboot 11687 } 11688 11689 callback(); 11690 }); 11691 } 11692 11693 function applyUpdates(oldobj, newobj, payload, log, callback) 11694 { 11695 var changed_datasets = false; 11696 11697 assert(log, 'no logger passed to applyUpdates()'); 11698 11699 // Note: oldobj is the VM *before* the update, newobj *after* 11700 log.debug('applying updates to ' + oldobj.uuid); 11701 11702 async.series([ 11703 function (cb) { 11704 if (payload.hasOwnProperty('update_disks') 11705 && oldobj.hasOwnProperty('disks')) { 11706 11707 resizeDisks(oldobj.disks, payload.update_disks, log, 11708 function (err, resized) { 11709 // If any were resized, mark that we changed something 11710 if (!err && resized > 0) { 11711 changed_datasets = true; 11712 } 11713 cb(err); 11714 } 11715 ); 11716 } else { 11717 cb(); 11718 } 11719 }, function (cb) { 11720 if (payload.hasOwnProperty('quota') 11721 && (Number(payload.quota) !== Number(oldobj.quota))) { 11722 11723 setQuota(newobj.zfs_filesystem, payload.quota, log, 11724 function (err) { 11725 11726 if (!err) { 11727 changed_datasets = true; 11728 } 11729 cb(err); 11730 }); 11731 } else { 11732 cb(); 11733 } 11734 }, function (cb) { 11735 // NOTE: we've already validated the value 11736 if (payload.hasOwnProperty('zfs_root_recsize') 11737 && (payload.zfs_root_recsize !== oldobj.zfs_root_recsize)) { 11738 11739 zfs(['set', 'recsize=' + payload.zfs_root_recsize, 11740 newobj.zfs_filesystem], log, function (err, fds) { 11741 11742 if (err) { 11743 log.error(err, 'failed to apply zfs_root_recsize: ' 11744 + fds.stderr); 11745 cb(new Error(rtrim(fds.stderr))); 11746 } else { 11747 cb(); 11748 } 11749 }); 11750 } else { 11751 cb(); 11752 } 11753 }, function (cb) { 11754 // NOTE: we've already validated the value. 11755 if (payload.hasOwnProperty('zfs_data_recsize') 11756 && oldobj.hasOwnProperty('zfs_data_recsize') 11757 && newobj.hasOwnProperty('datasets') 11758 && (newobj.datasets.indexOf(newobj.zfs_filesystem 11759 + '/data') !== -1)) { 11760 11761 zfs(['set', 'recsize=' + payload.zfs_data_recsize, 11762 newobj.zfs_filesystem + '/data'], log, function (err, fds) { 11763 11764 if (err) { 11765 log.error(err, 'failed to apply zfs_data_recsize: ' 11766 + fds.stderr); 11767 cb(new Error(rtrim(fds.stderr))); 11768 } else { 11769 cb(); 11770 } 11771 }); 11772 } else { 11773 cb(); 11774 } 11775 }, function (cb) { 11776 // NOTE: we've already validated the value 11777 if (payload.hasOwnProperty('zfs_root_compression') 11778 && (payload.zfs_root_compression !== 11779 oldobj.zfs_root_compression)) { 11780 11781 zfs(['set', 'compression=' + payload.zfs_root_compression, 11782 newobj.zfs_filesystem], log, function (err, fds) { 11783 11784 if (err) { 11785 log.error(err, 'failed to apply ' 11786 + 'zfs_root_compression: ' + fds.stderr); 11787 cb(new Error(rtrim(fds.stderr))); 11788 } else { 11789 cb(); 11790 } 11791 }); 11792 } else { 11793 cb(); 11794 } 11795 }, function (cb) { 11796 // NOTE: we've already validated the value 11797 if (payload.hasOwnProperty('zfs_data_compression') 11798 && newobj.hasOwnProperty('datasets') 11799 && (newobj.datasets.indexOf(newobj.zfs_filesystem 11800 + '/data') !== -1)) { 11801 11802 zfs(['set', 'compression=' + payload.zfs_data_compression, 11803 newobj.zfs_filesystem + '/data'], log, function (err, fds) { 11804 11805 if (err) { 11806 log.error(err, 'failed to apply ' 11807 + 'zfs_data_compression: ' + fds.stderr); 11808 cb(new Error(rtrim(fds.stderr))); 11809 } else { 11810 cb(); 11811 } 11812 }); 11813 } else { 11814 cb(); 11815 } 11816 }, function (cb) { 11817 var d; 11818 var disk; 11819 var zfs_updates = []; 11820 11821 if (payload.hasOwnProperty('update_disks')) { 11822 // loop through the disks we updated and perform any updates. 11823 for (disk in payload.update_disks) { 11824 disk = payload.update_disks[disk]; 11825 11826 if (!disk) { 11827 continue; 11828 } 11829 11830 for (d in oldobj.disks) { 11831 d = oldobj.disks[d]; 11832 if (d.path === disk.path 11833 && d.hasOwnProperty('zfs_filesystem')) { 11834 11835 if (disk.hasOwnProperty('compression')) { 11836 zfs_updates.push({ 11837 zfs_filesystem: d.zfs_filesystem, 11838 property: 'compression', 11839 value: disk.compression 11840 }); 11841 } 11842 11843 if (disk.hasOwnProperty('refreservation')) { 11844 zfs_updates.push({ 11845 zfs_filesystem: d.zfs_filesystem, 11846 property: 'refreservation', 11847 value: disk.refreservation + 'M' 11848 }); 11849 } 11850 } 11851 } 11852 } 11853 if (zfs_updates.length > 0) { 11854 log.debug('applying ' + zfs_updates.length 11855 + ' zfs updates'); 11856 async.each(zfs_updates, function (props, f_cb) { 11857 zfs(['set', props.property + '=' + props.value, 11858 props.zfs_filesystem], log, function (err, fds) { 11859 11860 if (err) { 11861 log.error(err, 'failed to set ' + props.property 11862 + '=' + props.value + ' for ' 11863 + props.zfs_filesystem); 11864 } 11865 f_cb(err); 11866 }); 11867 }, function (err) { 11868 log.debug({err: err}, 'end of zfs updates'); 11869 cb(err); 11870 }); 11871 } else { 11872 log.debug('no zfs updates to apply'); 11873 cb(); 11874 } 11875 } else { 11876 cb(); 11877 } 11878 }, function (cb) { 11879 var factor; 11880 var keys = []; 11881 var rctl; 11882 var rctls = { 11883 'cpu_shares': ['zone.cpu-shares'], 11884 'zfs_io_priority': ['zone.zfs-io-priority'], 11885 'max_lwps': ['zone.max-lwps'], 11886 'max_physical_memory': ['zone.max-physical-memory', 11887 (1024 * 1024)], 11888 'max_locked_memory': ['zone.max-locked-memory', (1024 * 1024)], 11889 'max_swap': ['zone.max-swap', (1024 * 1024)], 11890 'cpu_cap': ['zone.cpu-cap'] 11891 }; 11892 11893 if (!BRAND_OPTIONS[oldobj.brand].features.update_rctls) { 11894 cb(); 11895 return; 11896 } 11897 11898 for (rctl in rctls) { 11899 keys.push(rctl); 11900 } 11901 11902 async.forEachSeries(keys, function (prop, c) { 11903 rctl = rctls[prop][0]; 11904 if (rctls[prop][1]) { 11905 factor = rctls[prop][1]; 11906 } else { 11907 factor = 1; 11908 } 11909 11910 if (payload.hasOwnProperty(prop)) { 11911 setRctl(newobj.zonename, rctl, 11912 Number(payload[prop]) * factor, log, 11913 function (err) { 11914 if (err) { 11915 log.warn(err, 'failed to set rctl: ' + prop); 11916 } 11917 c(); 11918 } 11919 ); 11920 } else { 11921 c(); 11922 } 11923 }, function (err) { 11924 cb(err); 11925 }); 11926 }, function (cb) { 11927 if ((payload.hasOwnProperty('vnc_password') 11928 && (oldobj.vnc_password !== newobj.vnc_password)) 11929 || (payload.hasOwnProperty('vnc_port') 11930 && (oldobj.vnc_port !== newobj.vnc_port))) { 11931 11932 // tell vmadmd to refresh_password and port (will restart 11933 // listener) 11934 postVmadmd(newobj.uuid, 'reload_display', {}, log, 11935 function (e) { 11936 11937 if (e) { 11938 cb(new Error('Unable to tell vmadmd to reload VNC: ' 11939 + e.message)); 11940 } else { 11941 cb(); 11942 } 11943 }); 11944 } else if ((payload.hasOwnProperty('spice_password') 11945 && (oldobj.spice_password !== newobj.spice_password)) 11946 || (payload.hasOwnProperty('spice_port') 11947 && (oldobj.spice_port !== newobj.spice_port))) { 11948 11949 // tell vmadmd to refresh_password and port (will restart 11950 // listener) 11951 postVmadmd(newobj.uuid, 'reload_display', {}, log, 11952 function (e) { 11953 11954 if (e) { 11955 cb(new Error('Unable to tell vmadmd to reload SPICE: ' 11956 + e.message)); 11957 } else { 11958 cb(); 11959 } 11960 }); 11961 } else { 11962 cb(); 11963 } 11964 }, function (cb) { 11965 // we do this last, since we need the memory in the zone updated 11966 // first if we're growing this. 11967 if (payload.hasOwnProperty('tmpfs')) { 11968 resizeTmp(newobj.zonename, payload.tmpfs, log, cb); 11969 } else { 11970 cb(); 11971 } 11972 }, function (cb) { 11973 var now = new Date(); 11974 11975 // If we changed any dataset properties, we touch the zone's xml 11976 // file so that last_modified is correct. 11977 if (changed_datasets && newobj.hasOwnProperty('zonename')) { 11978 fs.utimes('/etc/zones/' + newobj.zonename + '.xml', now, now, 11979 function (err) { 11980 if (err) { 11981 log.warn(err, 'Unable to "touch" xml file for "' 11982 + newobj.zonename + '": ' + err.message); 11983 } else { 11984 log.debug('Touched ' + newobj.zonename 11985 + '.xml after datasets were modified.'); 11986 } 11987 // We don't error out if we just couldn't touch because 11988 // the actual updates above already did happen. 11989 cb(); 11990 } 11991 ); 11992 } else { 11993 cb(); 11994 } 11995 } 11996 11997 ], function (err, res) { 11998 log.debug('done applying updates to ' + oldobj.uuid); 11999 callback(err); 12000 }); 12001 } 12002 12003 exports.update = function (uuid, payload, options, callback) 12004 { 12005 var log; 12006 var new_vmobj; 12007 var vmobj; 12008 12009 // options parameter is optional 12010 if (arguments.length === 3) { 12011 callback = arguments[2]; 12012 options = {}; 12013 } 12014 12015 ensureLogging(true); 12016 if (options.hasOwnProperty('log')) { 12017 log = options.log; 12018 } else { 12019 log = VM.log.child({action: 'update', vm: uuid}); 12020 } 12021 12022 log.info('Updating VM ' + uuid + ' with initial payload:\n' 12023 + JSON.stringify(payload, null, 2)); 12024 12025 async.series([ 12026 function (cb) { 12027 // for update we currently always load the whole vmobj since the 12028 // update functions may need to look at bits from the existing VM. 12029 VM.load(uuid, {log: log}, function (err, obj) { 12030 if (err) { 12031 cb(err); 12032 return; 12033 } 12034 vmobj = obj; 12035 cb(); 12036 }); 12037 }, function (cb) { 12038 normalizePayload(payload, vmobj, log, function (e) { 12039 log.debug('Used payload:\n' 12040 + JSON.stringify(payload, null, 2)); 12041 cb(e); 12042 }); 12043 }, function (cb) { 12044 var deletables = []; 12045 var to_remove = []; 12046 var n; 12047 12048 // destroy remove_disks before we add in case we're recreating with 12049 // an existing name. 12050 12051 if (payload.hasOwnProperty('remove_disks')) { 12052 to_remove = payload.remove_disks; 12053 for (n in vmobj.disks) { 12054 if (to_remove.indexOf(vmobj.disks[n].path) !== -1) { 12055 deletables.push(vmobj.disks[n]); 12056 } 12057 } 12058 } else { 12059 // no disks to remove so all done. 12060 cb(); 12061 return; 12062 } 12063 12064 function loggedDeleteVolume(volume, callbk) { 12065 return deleteVolume(volume, log, callbk); 12066 } 12067 12068 async.forEachSeries(deletables, loggedDeleteVolume, 12069 function (err) { 12070 if (err) { 12071 log.error(err, 'Unknown error deleting volumes: ' 12072 + err.message); 12073 cb(err); 12074 } else { 12075 log.info('successfully deleted volumes'); 12076 cb(); 12077 } 12078 } 12079 ); 12080 }, function (cb) { 12081 var disks = []; 12082 var matches; 12083 var n; 12084 var p; 12085 var used_disk_indexes = []; 12086 12087 // create any new volumes we need. 12088 if (payload.hasOwnProperty('add_disks')) { 12089 disks = payload.add_disks; 12090 } 12091 12092 // create a list of used indexes so we can find the free ones to 12093 // use in createVolume() 12094 if (vmobj.hasOwnProperty('disks')) { 12095 for (n in vmobj.disks) { 12096 matches = vmobj.disks[n].path.match(/^.*-disk(\d+)$/); 12097 if (matches) { 12098 used_disk_indexes.push(Number(matches[1])); 12099 } 12100 } 12101 } 12102 12103 // add the bits of payload createVolumes() needs. 12104 p = {'add_disks': disks}; 12105 p.uuid = uuid; 12106 if (vmobj.hasOwnProperty('zpool')) { 12107 p.zpool = vmobj.zpool; 12108 } 12109 p.used_disk_indexes = used_disk_indexes; 12110 createVolumes(p, log, function (e) { 12111 cb(e); 12112 }); 12113 }, function (cb) { 12114 updateMetadata(vmobj, payload, log, function (e) { 12115 cb(e); 12116 }); 12117 }, function (cb) { 12118 updateRoutes(vmobj, payload, log, function (e) { 12119 cb(e); 12120 }); 12121 }, function (cb) { 12122 var zcfg; 12123 // generate a payload and send as a file to zonecfg to update 12124 // the zone. 12125 zcfg = buildZonecfgUpdate(vmobj, payload, log); 12126 zonecfgFile(zcfg, ['-u', uuid], log, function (e, fds) { 12127 if (e) { 12128 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12129 'unable to update zonecfg'); 12130 } else { 12131 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12132 'updated zonecfg'); 12133 } 12134 cb(e); 12135 }); 12136 }, function (cb) { 12137 restartMetadataService(vmobj, payload, log, function (e) { 12138 cb(e); 12139 }); 12140 }, function (cb) { 12141 updateVnicProperties(uuid, vmobj, payload, log, function (e) { 12142 cb(e); 12143 }); 12144 }, function (cb) { 12145 // Update the firewall data 12146 updateFirewallData(payload, vmobj, log, cb); 12147 }, function (cb) { 12148 // Do another full reload (all fields) so we can compare in 12149 // applyUpdates() and decide what's changed that we need to apply. 12150 VM.load(uuid, {log: log}, function (e, newobj) { 12151 if (e) { 12152 cb(e); 12153 } else { 12154 new_vmobj = newobj; 12155 cb(); 12156 } 12157 }); 12158 }, function (cb) { 12159 applyUpdates(vmobj, new_vmobj, payload, log, function () { 12160 cb(); 12161 }); 12162 } 12163 ], function (e) { 12164 callback(e); 12165 }); 12166 }; 12167 12168 function kill(uuid, log, callback) 12169 { 12170 var load_fields; 12171 var unset_autoboot = 'set autoboot=false'; 12172 12173 assert(log, 'no logger passed to kill()'); 12174 12175 log.info('Killing VM ' + uuid); 12176 12177 load_fields = [ 12178 'brand', 12179 'state', 12180 'transition_to', 12181 'uuid' 12182 ]; 12183 12184 /* We load here to ensure this vm exists. */ 12185 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12186 if (err) { 12187 callback(err); 12188 return; 12189 } 12190 12191 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12192 unset_autoboot = 12193 'select attr name=vm-autoboot; set value=false; end'; 12194 } 12195 12196 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) { 12197 var msg = trim(fds.stderr); 12198 12199 if (msg.match(/zone is already halted$/)) { 12200 // remove transition marker, since vm is not running now. 12201 VM.unsetTransition(vmobj, {log: log}, function () { 12202 var new_err; 12203 12204 new_err = new Error('VM ' + vmobj.uuid + ' is already ' 12205 + 'not \'running\' (currently: ' + vmobj.state + ')'); 12206 new_err.code = 'ENOTRUNNING'; 12207 callback(new_err); 12208 }); 12209 } else if (e) { 12210 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12211 'failed to halt VM ' + uuid); 12212 callback(err, msg); 12213 } else { 12214 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12215 'zoneadm halted VM ' + uuid); 12216 zonecfg(['-u', uuid, unset_autoboot], log, 12217 function (error, unset_fds) { 12218 12219 if (error) { 12220 // The vm is dead at this point, erroring out here would 12221 // do no good, so we just log it. 12222 log.error({err: error, stdout: unset_fds.stdout, 12223 stderr: unset_fds.stderr}, 'killVM(): Failed to ' 12224 + unset_autoboot); 12225 } else { 12226 log.debug({stdout: unset_fds.stdout, 12227 stderr: unset_fds.stderr}, 'unset autoboot flag'); 12228 } 12229 if (vmobj.state === 'stopping') { 12230 // remove transition marker 12231 VM.unsetTransition(vmobj, {log: log}, function () { 12232 callback(null, msg); 12233 }); 12234 } else { 12235 callback(null, msg); 12236 } 12237 }); 12238 } 12239 }); 12240 }); 12241 } 12242 12243 function postVmadmd(uuid, action, args, log, callback) 12244 { 12245 var arg; 12246 var url_path = '/vm/' + uuid + '?action=' + action; 12247 var req; 12248 12249 assert(log, 'no logger passed to postVmadmd()'); 12250 12251 if (args) { 12252 for (arg in args) { 12253 if (args.hasOwnProperty(arg)) { 12254 url_path = url_path + '&' + arg + '=' + args[arg]; 12255 } 12256 } 12257 } 12258 12259 log.debug('HTTP POST ' + url_path); 12260 req = http.request( 12261 { method: 'POST', host: '127.0.0.1', port: '8080', path: url_path }, 12262 function (res) { 12263 12264 log.debug('HTTP STATUS: ' + res.statusCode); 12265 log.debug('HTTP HEADERS: ' + JSON.stringify(res.headers)); 12266 res.setEncoding('utf8'); 12267 res.on('data', function (chunk) { 12268 log.debug('HTTP BODY: ' + chunk); 12269 }); 12270 res.on('end', function () { 12271 log.debug('HTTP conversation has completed.'); 12272 callback(); 12273 }); 12274 } 12275 ); 12276 req.on('error', function (e) { 12277 log.error(e, 'HTTP error: ' + e.message); 12278 callback(e); 12279 }); 12280 req.end(); 12281 } 12282 12283 // options parameter is *REQUIRED* for VM.stop() 12284 exports.stop = function (uuid, options, callback) 12285 { 12286 var load_fields; 12287 var log; 12288 var unset_autoboot = 'set autoboot=false'; 12289 var vmobj; 12290 12291 load_fields = [ 12292 'brand', 12293 'state', 12294 'uuid', 12295 'zonename' 12296 ]; 12297 12298 if (!options) { 12299 options = {}; 12300 } 12301 12302 if (options.hasOwnProperty('force') && options.force) { 12303 ensureLogging(true); 12304 if (options.hasOwnProperty('log')) { 12305 log = options.log; 12306 } else { 12307 log = VM.log.child({action: 'stop-F', vm: uuid}); 12308 } 12309 kill(uuid, log, callback); 12310 return; 12311 } else { 12312 ensureLogging(true); 12313 if (options.hasOwnProperty('log')) { 12314 log = options.log; 12315 } else { 12316 log = VM.log.child({action: 'stop', vm: uuid}); 12317 } 12318 } 12319 12320 log.info('Stopping VM ' + uuid); 12321 12322 if (!options.timeout) { 12323 options.timeout = 180; 12324 } 12325 if (!options.transition_to) { 12326 options.transition_to = 'stopped'; 12327 } 12328 12329 async.series([ 12330 function (cb) { 12331 /* We load here to ensure this vm exists. */ 12332 VM.load(uuid, {log: log, fields: load_fields}, function (err, obj) { 12333 var new_err; 12334 12335 if (err) { 12336 log.error(err); 12337 cb(err); 12338 return; 12339 } else { 12340 vmobj = obj; 12341 if (vmobj.state !== 'running') { 12342 new_err = new Error('VM ' + vmobj.uuid + ' is already ' 12343 + 'not \'running\' (currently: ' + vmobj.state 12344 + ')'); 12345 new_err.code = 'ENOTRUNNING'; 12346 cb(new_err); 12347 } else { 12348 cb(); 12349 } 12350 } 12351 }); 12352 }, function (cb) { 12353 // When stopping a VM that uses vm_autoboot, we assume we also do 12354 // the stop through vmadmd. 12355 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12356 async.series([ 12357 function (callbk) { 12358 setTransition(vmobj, 'stopping', options.transition_to, 12359 (options.timeout * 1000), log, function (err) { 12360 12361 callbk(err); 12362 }); 12363 }, function (callbk) { 12364 postVmadmd(vmobj.uuid, 'stop', 12365 {'timeout': options.timeout}, log, function (err) { 12366 12367 if (err) { 12368 log.error(err); 12369 err.message = 'Unable to post "stop" to vmadmd:' 12370 + ' ' + err.message; 12371 } 12372 callbk(err); 12373 }); 12374 }, function (callbk) { 12375 12376 // different version for VMs 12377 unset_autoboot = 'select attr name=vm-autoboot; ' 12378 + 'set value=false; end'; 12379 12380 zonecfg(['-u', uuid, unset_autoboot], log, 12381 function (err, fds) { 12382 if (err) { 12383 // The vm is dead at this point, failing 12384 // here would do no good, so we just log it. 12385 log.error({err: err, stdout: fds.stdout, 12386 stderr: fds.stderr}, 'stop(): Failed to' 12387 + ' ' + unset_autoboot + ' for ' + uuid 12388 + ': ' + err.message); 12389 } else { 12390 log.info({stdout: fds.stdout, 12391 stderr: fds.stderr}, 'Stopped ' + uuid); 12392 } 12393 callbk(); 12394 } 12395 ); 12396 } 12397 ], function (err) { 12398 cb(err); 12399 }); 12400 } else { // no vm_autoboot / vmadmd stop 12401 cb(); 12402 } 12403 }, function (cb) { 12404 var args; 12405 12406 // joyent brand specific stuff 12407 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0', 12408 '-i', '5']; 12409 12410 // not using vm_autoboot means using the 'normal' boot process 12411 if (!BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12412 async.series([ 12413 function (callbk) { 12414 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 12415 execFile('/usr/sbin/zlogin', args, 12416 function (err, stdout, stderr) { 12417 12418 if (err) { 12419 log.error({'err': err, 'stdout': stdout, 12420 'stderr': stderr}, 'zlogin for ' 12421 + vmobj.zonename + ' exited with code' 12422 + err.code + ': ' + err.message); 12423 callbk(err); 12424 } else { 12425 log.debug('zlogin claims to have worked, ' 12426 + 'stdout:\n' + stdout + '\nstderr:\n' 12427 + stderr); 12428 callbk(); 12429 } 12430 }); 12431 }, function (callbk) { 12432 zonecfg(['-u', uuid, unset_autoboot], log, 12433 function (err, fds) { 12434 if (err) { 12435 // The vm is dead at this point, failing 12436 // do no good, so we just log it. 12437 log.warn({err: err, stdout: fds.stdout, 12438 stderr: fds.stderr}, 'Failed to ' 12439 + unset_autoboot + ' for ' + uuid); 12440 } else { 12441 log.info({stdout: fds.stdout, 12442 stderr: fds.stderr}, 'Stopped ' + uuid); 12443 } 12444 callbk(); 12445 } 12446 ); 12447 } 12448 ], function (err) { 12449 cb(err); 12450 }); 12451 } else { // using vmautoboot so won't shutdown from in the zone 12452 cb(); 12453 } 12454 }, function (cb) { 12455 // Verify it's shut down 12456 VM.waitForZoneState(vmobj, 'installed', {log: log}, 12457 function (err, result) { 12458 12459 if (err) { 12460 if (err.code === 'ETIMEOUT') { 12461 log.info(err, 'timeout waiting for zone to go to ' 12462 + '"installed"'); 12463 } else { 12464 log.error(err, 'unknown error waiting for zone to go' 12465 + ' "installed"'); 12466 } 12467 cb(err); 12468 } else { 12469 // zone got to stopped 12470 log.info('VM seems to have switched to "installed"'); 12471 cb(); 12472 } 12473 }); 12474 } 12475 ], function (err) { 12476 callback(err); 12477 }); 12478 }; 12479 12480 // sends several query-* commands to QMP to get details for a VM 12481 exports.info = function (uuid, types, options, callback) 12482 { 12483 var load_fields; 12484 var log; 12485 12486 // options is optional 12487 if (arguments.length === 3) { 12488 callback = arguments[2]; 12489 options = {}; 12490 } 12491 12492 ensureLogging(false); 12493 if (options.hasOwnProperty('log')) { 12494 log = options.log; 12495 } else { 12496 log = VM.log.child({action: 'info', vm: uuid}); 12497 } 12498 12499 load_fields = [ 12500 'brand', 12501 'state', 12502 'uuid' 12503 ]; 12504 12505 // load to ensure we're a VM 12506 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12507 var type; 12508 12509 if (err) { 12510 callback(err); 12511 return; 12512 } 12513 12514 if (!BRAND_OPTIONS[vmobj.brand].features.runtime_info) { 12515 // XXX if support is added to other brands, update this message. 12516 callback(new Error('the info command is only supported for KVM ' 12517 + 'VMs')); 12518 return; 12519 } 12520 12521 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') { 12522 callback(new Error('Unable to get info for vm from state "' 12523 + vmobj.state + '", must be "running" or "stopping".')); 12524 return; 12525 } 12526 12527 if (!types) { 12528 types = ['all']; 12529 } 12530 12531 for (type in types) { 12532 type = types[type]; 12533 if (VM.INFO_TYPES.indexOf(type) === -1) { 12534 callback(new Error('unknown info type: ' + type)); 12535 return; 12536 } 12537 } 12538 12539 http.get({ host: '127.0.0.1', port: 8080, path: '/vm/' + uuid + '/info' 12540 + '?types=' + types.join(',') }, function (res) { 12541 12542 var data = ''; 12543 12544 if (res.statusCode !== 200) { 12545 callback(new Error('Unable to get info from vmadmd, query ' 12546 + 'returned ' + res.statusCode + '.')); 12547 } else { 12548 res.on('data', function (d) { 12549 data = data + d.toString(); 12550 }); 12551 res.on('end', function (d) { 12552 callback(null, JSON.parse(data)); 12553 }); 12554 } 12555 } 12556 ).on('error', function (e) { 12557 log.error(e); 12558 callback(e); 12559 }); 12560 }); 12561 }; 12562 12563 function reset(uuid, log, callback) 12564 { 12565 var load_fields; 12566 12567 assert(log, 'no logger passed to reset()'); 12568 12569 log.info('Resetting VM ' + uuid); 12570 12571 load_fields = [ 12572 'brand', 12573 'state', 12574 'uuid' 12575 ]; 12576 12577 /* We load here to ensure this vm exists. */ 12578 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12579 if (err) { 12580 callback(err); 12581 return; 12582 } 12583 12584 if (vmobj.state !== 'running') { 12585 callback(new Error('Cannot reset vm from state "' 12586 + vmobj.state + '", must be "running".')); 12587 return; 12588 } 12589 12590 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) { 12591 postVmadmd(vmobj.uuid, 'reset', {}, log, function (e) { 12592 if (e) { 12593 callback(new Error('Unable to post "reset" to ' 12594 + 'vmadmd: ' + e.message)); 12595 } else { 12596 callback(); 12597 } 12598 }); 12599 } else { 12600 zoneadm(['-u', vmobj.uuid, 'reboot', '-X'], log, function (e, fds) { 12601 if (e) { 12602 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12603 'zoneadm failed to reboot VM ' + vmobj.uuid); 12604 callback(new Error(rtrim(fds.stderr))); 12605 } else { 12606 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12607 'zoneadm rebooted VM ' + vmobj.uuid); 12608 callback(); 12609 } 12610 }); 12611 } 12612 }); 12613 } 12614 12615 // options is *REQUIRED* for VM.reboot() 12616 exports.reboot = function (uuid, options, callback) 12617 { 12618 var cleanup; 12619 var log; 12620 var reboot_async = false; 12621 var reboot_complete = false; 12622 var vmobj; 12623 12624 if (options.hasOwnProperty('log')) { 12625 log = options.log; 12626 } 12627 12628 if (options.hasOwnProperty('force') && options.force) { 12629 ensureLogging(true); 12630 if (!log) { 12631 log = VM.log.child({action: 'reboot-F', vm: uuid}); 12632 } 12633 reset(uuid, log, callback); 12634 return; 12635 } else { 12636 ensureLogging(true); 12637 log = VM.log.child({action: 'reboot', vm: uuid}); 12638 } 12639 12640 log.info('Rebooting VM ' + uuid); 12641 12642 if (!options) { 12643 options = {}; 12644 } 12645 12646 async.series([ 12647 function (cb) { 12648 var load_fields = [ 12649 'brand', 12650 'nics', 12651 'state', 12652 'zonename' 12653 ]; 12654 12655 VM.load(uuid, {fields: load_fields, log: log}, 12656 function (err, obj) { 12657 12658 if (err) { 12659 cb(err); 12660 return; 12661 } 12662 12663 if (obj.state !== 'running') { 12664 cb(new Error('Cannot reboot vm from state "' + obj.state 12665 + '", must be "running"')); 12666 return; 12667 } 12668 12669 vmobj = obj; 12670 cb(); 12671 }); 12672 }, function (cb) { 12673 // If nic tags have disappeared out from under us, don't allow a 12674 // reboot that will put us into a bad state 12675 lookupInvalidNicTags(vmobj.nics, log, function (e) { 12676 if (e) { 12677 cb(new Error('Cannot reboot vm: ' + e.message)); 12678 return; 12679 } 12680 12681 cb(); 12682 }); 12683 12684 }, function (cb) { 12685 var watcherobj; 12686 12687 if (!reboot_async) { 12688 watcherobj = watchZoneTransitions(function (err, ze) { 12689 if (!err && ze.zonename !== vmobj.zonename) { 12690 // not something we need to handle 12691 return; 12692 } 12693 12694 if (err) { 12695 // XXX what should we do here? 12696 log.error(err); 12697 return; 12698 } 12699 12700 log.debug(ze); // TODO move to trace 12701 12702 if (ze.newstate === 'running' 12703 && ze.oldstate !== 'running') { 12704 12705 if (watcherobj) { 12706 // cleanup our watcher since we found what we're 12707 // looking for. 12708 cleanup(); 12709 } 12710 12711 reboot_complete = true; 12712 } 12713 }, log); 12714 cleanup = watcherobj.cleanup; 12715 } 12716 12717 cb(); 12718 }, function (cb) { 12719 var args; 12720 12721 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) { 12722 // here we stop the machine and set a transition so vmadmd will 12723 // start the machine once the stop finished. 12724 options.transition_to = 'start'; 12725 options.log = log; 12726 VM.stop(uuid, options, function (err) { 12727 if (err) { 12728 cb(err); 12729 } else { 12730 cb(); 12731 } 12732 }); 12733 } else { 12734 // joyent branded zones 12735 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0', 12736 '-i', '6']; 12737 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 12738 execFile('/usr/sbin/zlogin', args, 12739 function (err, stdout, stderr) { 12740 if (err) { 12741 log.error({'err': err, 'stdout': stdout, 12742 'stderr': stderr}, 'zlogin for ' + vmobj.zonename 12743 + ' exited with code' + err.code + ': ' 12744 + err.message); 12745 cb(err); 12746 } else { 12747 cb(); 12748 } 12749 }); 12750 } 12751 }, function (cb) { 12752 var ival; 12753 var ticks = 0; 12754 12755 if (reboot_async) { 12756 cb(); 12757 return; 12758 } else { 12759 ticks = 180 * 10; // (180 * 10) 100ms ticks = 3m 12760 ival = setInterval(function () { 12761 if (reboot_complete) { 12762 log.debug('reboot marked complete, cleaning up'); 12763 clearInterval(ival); 12764 cleanup(); 12765 cb(); 12766 return; 12767 } 12768 ticks--; 12769 if (ticks <= 0) { 12770 // timed out 12771 log.debug('reboot timed out, cleaning up'); 12772 clearInterval(ival); 12773 cleanup(); 12774 cb(new Error('timed out waiting for zone to reboot')); 12775 return; 12776 } 12777 }, 100); 12778 } 12779 } 12780 ], function (err) { 12781 callback(err); 12782 }); 12783 }; 12784 12785 // options is *REQUIRED* for VM.sysrq 12786 exports.sysrq = function (uuid, req, options, callback) 12787 { 12788 var load_fields; 12789 var log; 12790 12791 ensureLogging(true); 12792 12793 if (options.hasOwnProperty('log')) { 12794 log = options.log; 12795 } else { 12796 log = VM.log.child({action: 'sysrq-' + req, vm: uuid}); 12797 } 12798 12799 log.info('Sending sysrq "' + req + '" to ' + uuid); 12800 12801 load_fields = [ 12802 'brand', 12803 'state', 12804 'uuid' 12805 ]; 12806 12807 /* We load here to ensure this vm exists. */ 12808 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12809 if (err) { 12810 callback(err); 12811 return; 12812 } 12813 12814 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') { 12815 callback(new Error('Unable to send request to vm from state "' 12816 + vmobj.state + '", must be "running" or "stopping".')); 12817 return; 12818 } 12819 12820 if (BRAND_OPTIONS[vmobj.brand].features.type !== 'KVM') { 12821 callback(new Error('The sysrq command is only supported for KVM.')); 12822 return; 12823 } 12824 12825 if (VM.SYSRQ_TYPES.indexOf(req) === -1) { 12826 callback(new Error('Invalid sysrq "' + req + '" valid values: ' 12827 + '"' + VM.SYSRQ_TYPES.join('","') + '".')); 12828 return; 12829 } 12830 12831 postVmadmd(vmobj.uuid, 'sysrq', {'request': req}, log, function (e) { 12832 if (e) { 12833 callback(new Error('Unable to post "sysrq" to vmadmd: ' 12834 + e.message)); 12835 } else { 12836 callback(); 12837 } 12838 }); 12839 }); 12840 }; 12841 12842 exports.console = function (uuid, options, callback) 12843 { 12844 var load_fields; 12845 var log; 12846 12847 // options is optional 12848 if (arguments.length === 2) { 12849 callback = arguments[1]; 12850 options = {}; 12851 } 12852 12853 ensureLogging(false); 12854 if (options.hasOwnProperty('log')) { 12855 log = options.log; 12856 } else { 12857 log = VM.log.child({action: 'console', vm: uuid}); 12858 } 12859 12860 load_fields = [ 12861 'brand', 12862 'state', 12863 'zonename', 12864 'zonepath' 12865 ]; 12866 12867 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12868 var args; 12869 var child; 12870 var cmd; 12871 var stty; 12872 12873 if (err) { 12874 callback(err); 12875 return; 12876 } 12877 if (vmobj.state !== 'running') { 12878 callback(new Error('cannot connect to console when state is ' 12879 + '"' + vmobj.state + '" must be "running".')); 12880 return; 12881 } 12882 12883 if (BRAND_OPTIONS[vmobj.brand].features.zlogin_console) { 12884 cmd = '/usr/sbin/zlogin'; 12885 args = ['-C', '-e', '\\035', vmobj.zonename]; 12886 12887 log.debug(cmd + ' ' + args.join(' ')); 12888 child = spawn(cmd, args, {customFds: [0, 1, 2]}); 12889 child.on('close', function (code) { 12890 log.debug('zlogin process exited with code ' + code); 12891 callback(); 12892 }); 12893 } else if (BRAND_OPTIONS[vmobj.brand].features.serial_console) { 12894 async.series([ 12895 function (cb) { 12896 cmd = '/usr/bin/stty'; 12897 args = ['-g']; 12898 stty = ''; 12899 12900 log.debug(cmd + ' ' + args.join(' ')); 12901 child = spawn(cmd, args, {customFds: [0, -1, -1]}); 12902 child.stdout.on('data', function (data) { 12903 // log.debug('data: ' + data.toString()); 12904 stty = data.toString(); 12905 }); 12906 child.on('close', function (code) { 12907 log.debug('stty process exited with code ' + code); 12908 cb(); 12909 }); 12910 }, function (cb) { 12911 cmd = '/usr/bin/socat'; 12912 args = ['unix-client:' + vmobj.zonepath 12913 + '/root/tmp/vm.console', '-,raw,echo=0,escape=0x1d']; 12914 12915 log.debug(cmd + ' ' + args.join(' ')); 12916 child = spawn(cmd, args, {customFds: [0, 1, 2]}); 12917 child.on('close', function (code) { 12918 log.debug('zlogin process exited with code ' + code); 12919 cb(); 12920 }); 12921 }, function (cb) { 12922 cmd = '/usr/bin/stty'; 12923 args = [stty]; 12924 12925 log.debug(cmd + ' ' + args.join(' ')); 12926 child = spawn(cmd, args, {customFds: [0, -1, -1]}); 12927 child.on('close', function (code) { 12928 log.debug('stty process exited with code ' + code); 12929 cb(); 12930 }); 12931 } 12932 ], function (e, results) { 12933 callback(e); 12934 }); 12935 } else { 12936 callback(new Error('Cannot get console for brand: ' + vmobj.brand)); 12937 } 12938 }); 12939 };