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 lock = require('/usr/vm/node_modules/locker').lock; 71 var EventEmitter = require('events').EventEmitter; 72 var exec = cp.exec; 73 var execFile = cp.execFile; 74 var expat = require('/usr/node/node_modules/node-expat'); 75 var fs = require('fs'); 76 var fw = require('/usr/fw/lib/fw'); 77 var http = require('http'); 78 var net = require('net'); 79 var path = require('path'); 80 var Qmp = require('/usr/vm/node_modules/qmp').Qmp; 81 var spawn = cp.spawn; 82 var sprintf = require('/usr/node/node_modules/sprintf').sprintf; 83 var tty = require('tty'); 84 var util = require('util'); 85 86 var log_to_file = false; 87 88 // keep the last 512 messages just in case we end up wanting them. 89 var ringbuffer = new bunyan.RingBuffer({ limit: 512 }); 90 91 // zfs_list_queue variables for the serialization of 'zfs list' calls 92 var zfs_list_in_progress = {}; 93 var zfs_list_queue; 94 95 // global handle for the zoneevent watcher 96 var zoneevent; 97 98 /* 99 * zone states from libzonecfg/common/zonecfg_impl.h 100 * 101 * #define ZONE_STATE_STR_CONFIGURED "configured" 102 * #define ZONE_STATE_STR_INCOMPLETE "incomplete" 103 * #define ZONE_STATE_STR_INSTALLED "installed" 104 * #define ZONE_STATE_STR_READY "ready" 105 * #define ZONE_STATE_STR_MOUNTED "mounted" 106 * #define ZONE_STATE_STR_RUNNING "running" 107 * #define ZONE_STATE_STR_SHUTTING_DOWN "shutting_down" 108 * #define ZONE_STATE_STR_DOWN "down" 109 * 110 */ 111 112 exports.FLATTENABLE_ARRAYS = [ 113 'resolvers' 114 ]; 115 exports.FLATTENABLE_ARRAY_HASH_KEYS = [ 116 'disks', 117 'nics' 118 ]; 119 exports.FLATTENABLE_HASH_KEYS = [ 120 'customer_metadata', 121 'internal_metadata', 122 'routes', 123 'tags' 124 ]; 125 126 var DEFAULT_MDATA_TIMEOUT = 300; 127 var DISABLED = 0; 128 var MAX_SNAPNAME_LENGTH = 64; 129 var MINIMUM_MAX_SWAP = 256; 130 var PROVISION_TIMEOUT = 300; 131 var STOP_TIMEOUT = 60; 132 var VM = this; 133 134 VM.log = null; 135 136 // can be (re)set by loader before we start. 137 exports.logger = null; 138 exports.loglevel = 'debug'; 139 140 // OpenOnErrorFileStream is a bunyan stream that only creates the file when 141 // there's an error or higher level message or when the global log_to_file 142 // variable is set. For actions that modify things log_to_file is always set. 143 // For other actions we shouldn't log in the normal case but where we do want 144 // logs when something breaks. Thanks to Trent++ for most of this code. 145 // 146 // Note: if you want to rotate the logs while this is writing to a file, you 147 // can first move it. The watcher will notice that the log file was moved and 148 // reopen a new file with the original name. 149 150 function OpenOnErrorFileStream(filename) { 151 this.path = filename; 152 this.write = this.constructor.prototype.write1; 153 this.end = this.constructor.prototype.end1; 154 this.emit = this.constructor.prototype.emit1; 155 this.once = this.constructor.prototype.once1; 156 157 this.newStream = function () { 158 var self = this; 159 var watcher; 160 161 self.stream = fs.createWriteStream(self.path, 162 {flags: 'a', encoding: 'utf8'}); 163 164 watcher = fs.watch(self.path, {persistent: false}, function (evt) { 165 if (evt != 'rename') { 166 return; 167 } 168 // file was renamed, we want to reopen. 169 if (self.stream) { 170 self.stream.destroySoon(); 171 } 172 watcher.close(); 173 self.stream = null; 174 }); 175 }; 176 } 177 178 OpenOnErrorFileStream.prototype.end1 = function () { 179 // in initial mode we're not writing anything, so nothing to flush 180 return; 181 }; 182 183 OpenOnErrorFileStream.prototype.emit1 = function () { 184 return; 185 }; 186 187 // Warning: never emits anything 188 OpenOnErrorFileStream.prototype.once1 = function () { 189 return; 190 }; 191 192 // used until first ERROR or higher, then opens file and ensures future writes 193 // go to .write2() 194 OpenOnErrorFileStream.prototype.write1 = function (rec) { 195 var r; 196 var stream; 197 198 if (rec.level >= bunyan.ERROR || log_to_file) { 199 if (! this.stream) { 200 this.newStream(); 201 } 202 203 stream = this.stream; 204 205 this.emit = function () { stream.emit.apply(stream, arguments); }; 206 this.end = function () { stream.end.apply(stream, arguments); }; 207 this.once = function () { stream.once.apply(stream, arguments); }; 208 this.write = this.constructor.prototype.write2; 209 // dump out logs from ringbuffer too since there was an error so we can 210 // figure out what's going on. 211 for (r in ringbuffer.records) { 212 r = ringbuffer.records[r]; 213 if (r != rec) { 214 this.write(r); 215 } 216 } 217 218 this.write(rec); 219 } 220 221 // This write doesn't fail (since it's going to memory or nowhere) so we 222 // always return true so that callers don't try to wait for 'drain' which 223 // we'll not emit. 224 return true; 225 }; 226 227 // used when writing to file 228 OpenOnErrorFileStream.prototype.write2 = function (rec) { 229 var str; 230 231 // need to support writing '' so we know when to drain 232 if (typeof (rec) === 'string' && rec.length < 1) { 233 str = ''; 234 } else { 235 str = JSON.stringify(rec, bunyan.safeCycles()) + '\n'; 236 } 237 238 if (! this.stream) { 239 this.newStream(); 240 } 241 242 return this.stream.write(str); 243 }; 244 245 // This function should be called by any exported function from this module. 246 // It ensures that a logger is setup. If side_effects is true, we'll start 247 // writing log messages to the file right away. If not, we'll only start 248 // logging after we hit a message error or higher. This is intended such that 249 // things that are expected to change the state or modify VMs on the system: 250 // eg. create, start, stop, delete should have this set true. It should be 251 // set false when the action should not cause changes to the system: 252 // eg.: load, lookup, info, console, &c. 253 function ensureLogging(side_effects) 254 { 255 side_effects = !!side_effects; // make it boolean (undef === false) 256 257 var filename; 258 var logname; 259 var streams = []; 260 261 function start_logging() { 262 var params = { 263 name: logname, 264 streams: streams, 265 serializers: bunyan.stdSerializers 266 }; 267 268 if (process.env.REQ_ID) { 269 params.req_id = process.env.REQ_ID; 270 } else if (process.env.req_id) { 271 params.req_id = process.env.req_id; 272 } 273 VM.log = bunyan.createLogger(params); 274 } 275 276 // This is here in case an app calls a lookup first and then a create. The 277 // logger will get created in no-sideeffects mode for the lookup but when 278 // the create is called this will force the switch to writing. 279 if (side_effects) { 280 log_to_file = true; 281 } 282 283 if (VM.log) { 284 // We're already logging, don't break things. 285 return; 286 } 287 288 if (VM.hasOwnProperty('logname')) { 289 logname = VM.logname.replace(/[^a-zA-Z\_]/g, ''); 290 } 291 if (!logname || logname.length < 1) { 292 logname = 'VM'; 293 } 294 295 if (VM.hasOwnProperty('logger') && VM.logger) { 296 // Use concat, in case someone's sneaky and makes more than one logger. 297 // We don't officially support that yet though. 298 streams = streams.concat(VM.logger); 299 } 300 301 // Add the ringbuffer which we'll dump if we switch from not writing to 302 // writing, and so that they'll show up in dumps. 303 streams.push({ 304 level: 'trace', 305 type: 'raw', 306 stream: ringbuffer 307 }); 308 309 try { 310 if (!fs.existsSync('/var/log/vm')) { 311 fs.mkdirSync('/var/log/vm'); 312 } 313 if (!fs.existsSync('/var/log/vm/logs')) { 314 fs.mkdirSync('/var/log/vm/logs'); 315 } 316 } catch (e) { 317 // We can't ever log to a file in /var/log/vm/logs if we can't create 318 // it, so we just log to ring buffer (above). 319 start_logging(); 320 return; 321 } 322 323 filename = '/var/log/vm/logs/' + Date.now(0) + '-' 324 + sprintf('%06d', process.pid) + '-' + logname + '.log'; 325 326 streams.push({ 327 type: 'raw', 328 stream: new OpenOnErrorFileStream(filename), 329 level: VM.loglevel 330 }); 331 332 start_logging(); 333 } 334 335 function ltrim(str, chars) 336 { 337 chars = chars || '\\s'; 338 str = str || ''; 339 return str.replace(new RegExp('^[' + chars + ']+', 'g'), ''); 340 } 341 342 function rtrim(str, chars) 343 { 344 chars = chars || '\\s'; 345 str = str || ''; 346 return str.replace(new RegExp('[' + chars + ']+$', 'g'), ''); 347 } 348 349 function trim(str, chars) 350 { 351 return ltrim(rtrim(str, chars), chars); 352 } 353 354 function isUUID(str) { 355 var re = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; 356 if (str && str.length === 36 && str.match(re)) { 357 return true; 358 } else { 359 return false; 360 } 361 } 362 363 function fixBoolean(str) 364 { 365 if (str === 'true') { 366 return true; 367 } else if (str === 'false') { 368 return false; 369 } else { 370 return str; 371 } 372 } 373 374 function fixBooleanLoose(str) 375 { 376 if (str === 'true' || str === '1' || str === 1) { 377 return true; 378 } else if (str === 'false' || str === '0' || str === 0) { 379 return false; 380 } else { 381 return str; 382 } 383 } 384 385 function isCIDR(str) { 386 if (typeof (str) !== 'string') { 387 return false; 388 } 389 var parts = str.split('/'); 390 if (parts.length !== 2 || !net.isIPv4(parts[0])) { 391 return false; 392 } 393 394 var size = Number(parts[1]); 395 if (!size || size < 8 || size > 32) { 396 return false; 397 } 398 399 return true; 400 } 401 402 // IMPORTANT: 403 // 404 // Some of these properties get translated below into backward compatible 405 // names. 406 // 407 408 var UPDATABLE_NIC_PROPS = [ 409 'primary', 410 'nic_tag', 411 'vrrp_vrid', 412 'vrrp_primary_ip', 413 'blocked_outgoing_ports', 414 'mac', 415 'gateway', 416 'ip', 417 'model', 418 'netmask', 419 'network_uuid', 420 'dhcp_server', 421 'allow_dhcp_spoofing', 422 'allow_ip_spoofing', 423 'allow_mac_spoofing', 424 'allow_restricted_traffic', 425 'allow_unfiltered_promisc', 426 'vlan_id' 427 ]; 428 429 var UPDATABLE_DISK_PROPS = [ 430 'boot', 431 'model' 432 ]; 433 434 // Note: this doesn't include 'state' because of 'stopping' which is a virtual 435 // state and therefore lookups would be wrong (because they'd search on real 436 // state). 437 var QUICK_LOOKUP = [ 438 'zoneid', 439 'zonename', 440 'zonepath', 441 'uuid', 442 'brand', 443 'ip_type' 444 ]; 445 446 exports.DISK_MODELS = [ 447 'virtio', 448 'ide', 449 'scsi' 450 ]; 451 452 exports.VGA_TYPES = [ 453 'cirrus', 454 'std', 455 'vmware', 456 'qxl', 457 'xenfb' 458 ]; 459 460 exports.INFO_TYPES = [ 461 'all', 462 'block', 463 'blockstats', 464 'chardev', 465 'cpus', 466 'kvm', 467 'pci', 468 'spice', 469 'status', 470 'version', 471 'vnc' 472 ]; 473 474 exports.SYSRQ_TYPES = [ 475 'nmi', 476 'screenshot' 477 ]; 478 479 exports.COMPRESSION_TYPES = [ 480 'on', 481 'off', 482 'lzjb', 483 'gzip', 484 'gzip-1', 485 'gzip-2', 486 'gzip-3', 487 'gzip-4', 488 'gzip-5', 489 'gzip-6', 490 'gzip-7', 491 'gzip-8', 492 'gzip-9', 493 'zle' 494 ]; 495 496 exports.KVM_MEM_OVERHEAD = 1024; 497 exports.KVM_MIN_MEM_OVERHEAD = 256; 498 499 var XML_PROPERTIES = { 500 'zone': { 501 'name': 'zonename', 502 'zonepath': 'zonepath', 503 'autoboot': 'autoboot', 504 'brand': 'brand', 505 'limitpriv': 'limit_priv', 506 'fs-allowed': 'fs_allowed' 507 }, 508 'zone.attr': { 509 'alias': 'alias', 510 'archive-on-delete': 'archive_on_delete', 511 'billing-id': 'billing_id', 512 'boot': 'boot', 513 'cpu-type': 'cpu_type', 514 'create-timestamp': 'create_timestamp', 515 'dataset-uuid': 'image_uuid', 516 'default-gateway': 'default_gateway', 517 'disk-driver': 'disk_driver', 518 'dns-domain': 'dns_domain', 519 'do-not-inventory': 'do_not_inventory', 520 'failed': 'failed', 521 'firewall-enabled': 'firewall_enabled', 522 'hostname': 'hostname', 523 'init-name': 'init_name', 524 'never-booted': 'never_booted', 525 'nic-driver': 'nic_driver', 526 'owner-uuid': 'owner_uuid', 527 'package-name': 'package_name', 528 'package-version': 'package_version', 529 'qemu-extra-opts': 'qemu_extra_opts', 530 'qemu-opts': 'qemu_opts', 531 'ram': 'ram', 532 'restart-init': 'restart_init', 533 'resolvers': 'resolvers', 534 'spice-opts': 'spice_opts', 535 'spice-password': 'spice_password', 536 'spice-port': 'spice_port', 537 'tmpfs': 'tmpfs', 538 'transition': 'transition', 539 'vcpus': 'vcpus', 540 'vga': 'vga', 541 'virtio-txtimer': 'virtio_txtimer', 542 'virtio-txburst': 'virtio_txburst', 543 'vm-version': 'v', 544 'vm-autoboot': 'vm_autoboot', 545 'vnc-password': 'vnc_password', 546 'vnc-port': 'vnc_port' 547 }, 548 'zone.rctl.zone.cpu-shares.rctl-value': { 549 'limit': 'cpu_shares' 550 }, 551 'zone.rctl.zone.cpu-cap.rctl-value': { 552 'limit': 'cpu_cap' 553 }, 554 'zone.rctl.zone.zfs-io-priority.rctl-value': { 555 'limit': 'zfs_io_priority' 556 }, 557 'zone.rctl.zone.max-lwps.rctl-value': { 558 'limit': 'max_lwps' 559 }, 560 'zone.rctl.zone.max-physical-memory.rctl-value': { 561 'limit': 'max_physical_memory' 562 }, 563 'zone.rctl.zone.max-locked-memory.rctl-value': { 564 'limit': 'max_locked_memory' 565 }, 566 'zone.rctl.zone.max-swap.rctl-value': { 567 'limit': 'max_swap' 568 }, 569 'nic': { 570 'ip': 'ip', 571 'mac-addr': 'mac', 572 'physical': 'interface', 573 'vlan-id': 'vlan_id', 574 'global-nic': 'nic_tag', 575 'dhcp_server': 'dhcp_server', 576 'allow_dhcp_spoofing': 'allow_dhcp_spoofing', 577 'allow_ip_spoofing': 'allow_ip_spoofing', 578 'allow_mac_spoofing': 'allow_mac_spoofing', 579 'allow_restricted_traffic': 'allow_restricted_traffic', 580 'allow_unfiltered_promisc': 'allow_unfiltered_promisc', 581 'allowed_ips': 'allowed_ips', 582 'netmask': 'netmask', 583 'network_uuid': 'network_uuid', 584 'model': 'model', 585 'gateway': 'gateway', 586 'primary': 'primary', 587 'vrrp_vrid': 'vrrp_vrid', 588 'vrrp_primary_ip': 'vrrp_primary_ip', 589 'blocked-outgoing-ports': 'blocked_outgoing_ports' 590 }, 591 'filesystem': { 592 'special': 'source', 593 'directory': 'target', 594 'type': 'type', 595 'raw': 'raw' 596 }, 597 'disk': { 598 'boot': 'boot', 599 'image-size': 'image_size', 600 'image-name': 'image_name', 601 'image-uuid': 'image_uuid', 602 'match': 'path', 603 'media': 'media', 604 'model': 'model', 605 'size': 'size' 606 } 607 }; 608 609 /* 610 * This allows one to define a function that will be run over the values from 611 * the zonecfg at the point where we transform that data into a VM object. 612 * 613 */ 614 var XML_PROPERTY_TRANSFORMS = { 615 'alias': unbase64, 616 'archive_on_delete': fixBoolean, 617 'autoboot': fixBoolean, 618 'cpu_cap': numberify, 619 'cpu_shares': numberify, 620 'disks': { 621 'boot': fixBoolean, 622 'image_size': numberify, 623 'size': numberify 624 }, 625 'do_not_inventory': fixBoolean, 626 'firewall_enabled': fixBoolean, 627 'max_locked_memory': unmangleMem, 628 'max_lwps': numberify, 629 'max_physical_memory': unmangleMem, 630 'max_swap': unmangleMem, 631 'never_booted': fixBoolean, 632 'nics': { 633 'dhcp_server': fixBoolean, 634 'allow_dhcp_spoofing': fixBoolean, 635 'allow_ip_spoofing': fixBoolean, 636 'allow_mac_spoofing': fixBoolean, 637 'allow_restricted_traffic': fixBoolean, 638 'allow_unfiltered_promisc': fixBoolean, 639 'allowed_ips': separateCommas, 640 'primary': fixBooleanLoose, 641 'vrrp_vrid': numberify, 642 'vlan_id': numberify 643 }, 644 'qemu_extra_opts': unbase64, 645 'qemu_opts': unbase64, 646 'ram': numberify, 647 'restart_init': fixBoolean, 648 'resolvers': separateCommas, 649 'spice_password': unbase64, 650 'spice_port': numberify, 651 'spice_opts': unbase64, 652 'tmpfs': numberify, 653 'v': numberify, 654 'vcpus': numberify, 655 'virtio_txburst': numberify, 656 'virtio_txtimer': numberify, 657 'vnc_password': unbase64, 658 'vnc_port': numberify, 659 'zfs_io_priority': numberify, 660 'zoneid': numberify 661 }; 662 663 /* 664 * This defines all of the possible properties that could be in a create/update 665 * payload and their types. Each of the entries are required to have at least 666 * a 'type' property which is one of: 667 * 668 * object-array -- an array of objects 669 * boolean -- true or false 670 * flat-object -- an object that has only string properties 671 * integer -- integers only 672 * list -- Either comma separated or array list of strings 673 * string -- Simple string 674 * uuid -- A standard 00000000-0000-0000-0000-000000000000 type uuid 675 * zpool -- The name of an existing zpool 676 * 677 */ 678 var PAYLOAD_PROPERTIES = { 679 'add_disks': {'type': 'object-array', 'check_as': 'disks'}, 680 'add_nics': {'type': 'object-array', 'check_as': 'nics'}, 681 'alias': {'type': 'string'}, 682 'archive_on_delete': {'type': 'boolean'}, 683 'autoboot': {'type': 'boolean'}, 684 'billing_id': {'type': 'string'}, 685 'boot': {'type': 'string'}, 686 'brand': {'type': 'string'}, 687 'cpu_cap': {'type': 'integer'}, 688 'cpu_shares': {'type': 'integer'}, 689 'cpu_type': {'type': 'string'}, 690 'create_only': {'type': 'boolean'}, 691 'create_timestamp': {'type': 'string'}, 692 'customer_metadata': {'type': 'flat-object'}, 693 'dataset_uuid': {'type': 'uuid'}, 694 'delegate_dataset': {'type': 'boolean'}, 695 'disks': {'type': 'object-array'}, 696 'disks.*.block_size': {'type': 'integer'}, 697 'disks.*.boot': {'type': 'boolean'}, 698 'disks.*.compression': {'type': 'string'}, 699 'disks.*.image_name': {'type': 'string'}, 700 'disks.*.image_size': {'type': 'integer'}, 701 'disks.*.image_uuid': {'type': 'uuid'}, 702 'disks.*.refreservation': {'type': 'integer'}, 703 'disks.*.size': {'type': 'integer'}, 704 'disks.*.media': {'type': 'string'}, 705 'disks.*.model': {'type': 'string'}, 706 'disks.*.nocreate': {'type': 'boolean'}, 707 'disks.*.path': {'type': 'string'}, 708 'disks.*.zpool': {'type': 'zpool'}, 709 'disk_driver': {'type': 'string'}, 710 'do_not_inventory': {'type': 'boolean'}, 711 'dns_domain': {'type': 'string'}, 712 'filesystems': {'type': 'object-array'}, 713 'filesystems.*.type': {'type': 'string'}, 714 'filesystems.*.source': {'type': 'string'}, 715 'filesystems.*.target': {'type': 'string'}, 716 'filesystems.*.raw': {'type': 'string'}, 717 'filesystems.*.options': {'type': 'list'}, 718 'firewall': {'type': 'object'}, 719 'firewall_enabled': {'type': 'boolean'}, 720 'fs_allowed': {'type': 'list'}, 721 'hostname': {'type': 'string'}, 722 'image_uuid': {'type': 'uuid'}, 723 'init_name': {'type': 'string'}, 724 'internal_metadata': {'type': 'flat-object'}, 725 'limit_priv': {'type': 'list'}, 726 'max_locked_memory': {'type': 'integer'}, 727 'max_lwps': {'type': 'integer'}, 728 'max_physical_memory': {'type': 'integer'}, 729 'max_swap': {'type': 'integer'}, 730 'mdata_exec_timeout': {'type': 'integer'}, 731 'nics': {'type': 'object-array'}, 732 'nics.*.allow_dhcp_spoofing': {'type': 'boolean'}, 733 'nics.*.allow_ip_spoofing': {'type': 'boolean'}, 734 'nics.*.allow_mac_spoofing': {'type': 'boolean'}, 735 'nics.*.allow_restricted_traffic': {'type': 'boolean'}, 736 'nics.*.allow_unfiltered_promisc': {'type': 'boolean'}, 737 'nics.*.allowed_ips': {'type': 'list'}, 738 'nics.*.blocked_outgoing_ports': {'type': 'list'}, 739 'nics.*.dhcp_server': {'type': 'boolean'}, 740 'nics.*.gateway': {'type': 'string'}, 741 'nics.*.interface': {'type': 'string'}, 742 'nics.*.ip': {'type': 'string'}, 743 'nics.*.mac': {'type': 'string'}, 744 'nics.*.model': {'type': 'string'}, 745 'nics.*.netmask': {'type': 'string'}, 746 'nics.*.network_uuid': {'type': 'uuid'}, 747 'nics.*.nic_tag': {'type': 'string'}, 748 'nics.*.primary': {'type': 'boolean'}, 749 'nics.*.vrrp_vrid': {'type': 'integer-8bit'}, 750 'nics.*.vrrp_primary_ip': {'type': 'string'}, 751 'nics.*.vlan_id': {'type': 'integer'}, 752 'nic_driver': {'type': 'string'}, 753 'nowait': {'type': 'boolean'}, 754 'owner_uuid': {'type': 'string'}, 755 'package_name': {'type': 'string'}, 756 'package_version': {'type': 'string'}, 757 'qemu_opts': {'type': 'string'}, 758 'qemu_extra_opts': {'type': 'string'}, 759 'quota': {'type': 'integer'}, 760 'ram': {'type': 'integer'}, 761 'remove_customer_metadata': {'type': 'list'}, 762 'remove_disks': {'type': 'list'}, 763 'remove_internal_metadata': {'type': 'list'}, 764 'remove_nics': {'type': 'list'}, 765 'remove_routes': {'type': 'list'}, 766 'remove_tags': {'type': 'list'}, 767 'restart_init': {'type': 'boolean'}, 768 'resolvers': {'type': 'list'}, 769 'routes': {'type': 'flat-object'}, 770 'set_routes': {'type': 'flat-object'}, 771 'set_tags': {'type': 'flat-object'}, 772 'set_customer_metadata': {'type': 'flat-object'}, 773 'set_internal_metadata': {'type': 'flat-object'}, 774 'spice_opts': {'type': 'string'}, 775 'spice_password': {'type': 'string'}, 776 'spice_port': {'type': 'integer'}, 777 'tags': {'type': 'flat-object'}, 778 'tmpfs': {'type': 'integer'}, 779 'transition': {'type': 'flat-object'}, 780 'update_disks': {'type': 'object-array', 'check_as': 'disks'}, 781 'update_nics': {'type': 'object-array', 'check_as': 'nics'}, 782 'uuid': {'type': 'uuid'}, 783 'v': {'type': 'integer'}, 784 'vcpus': {'type': 'integer'}, 785 'vga': {'type': 'string'}, 786 'virtio_txburst': {'type': 'integer'}, 787 'virtio_txtimer': {'type': 'integer'}, 788 'vnc_password': {'type': 'string'}, 789 'vnc_port': {'type': 'integer'}, 790 'zfs_data_compression': {'type': 'string'}, 791 'zfs_data_recsize': {'type': 'integer'}, 792 'zfs_io_priority': {'type': 'integer'}, 793 'zfs_root_compression': {'type': 'string'}, 794 'zfs_root_recsize': {'type': 'integer'}, 795 'zone_dataset_uuid': {'type': 'uuid'}, 796 'zonename': {'type': 'string'}, 797 'zfs_storage_pool_name': {'type': 'zpool'}, 798 'zpool': {'type': 'zpool'} 799 }; 800 801 // shared between 'joyent' and 'joyent-minimal' 802 var joyent_allowed = { 803 'add_nics': ['update'], 804 'alias': ['create', 'receive', 'update'], 805 'archive_on_delete': ['create', 'receive', 'update'], 806 'autoboot': ['create', 'receive', 'update'], 807 'billing_id': ['create', 'receive', 'update'], 808 'brand': ['create', 'receive'], 809 'cpu_cap': ['create', 'receive', 'update'], 810 'cpu_shares': ['create', 'receive', 'update'], 811 'create_only': ['receive'], 812 'create_timestamp': ['receive'], 813 'customer_metadata': ['create', 'receive'], 814 'dataset_uuid': ['create', 'receive'], 815 'delegate_dataset': ['create', 'receive'], 816 'do_not_inventory': ['create', 'receive', 'update'], 817 'dns_domain': ['create', 'receive'], 818 'filesystems': ['create', 'receive'], 819 'filesystems.*.type': ['add'], 820 'filesystems.*.source': ['add'], 821 'filesystems.*.target': ['add'], 822 'filesystems.*.raw': ['add'], 823 'filesystems.*.options': ['add'], 824 'firewall': ['create'], 825 'firewall_enabled': ['create', 'receive', 'update'], 826 'fs_allowed': ['create', 'receive', 'update'], 827 'hostname': ['create', 'receive', 'update'], 828 'image_uuid': ['create', 'receive'], 829 'init_name': ['create', 'receive', 'update'], 830 'internal_metadata': ['create', 'receive'], 831 'limit_priv': ['create', 'receive', 'update'], 832 'max_locked_memory': ['create', 'receive', 'update'], 833 'max_lwps': ['create', 'receive', 'update'], 834 'max_physical_memory': ['create', 'receive', 'update'], 835 'max_swap': ['create', 'receive', 'update'], 836 'mdata_exec_timeout': ['create'], 837 'nics': ['create', 'receive'], 838 'nics.*.allow_dhcp_spoofing': ['add', 'update'], 839 'nics.*.allow_ip_spoofing': ['add', 'update'], 840 'nics.*.allow_mac_spoofing': ['add', 'update'], 841 'nics.*.allow_restricted_traffic': ['add', 'update'], 842 'nics.*.allowed_ips': ['add', 'update'], 843 'nics.*.blocked_outgoing_ports': ['add', 'update'], 844 'nics.*.dhcp_server': ['add', 'update'], 845 'nics.*.gateway': ['add', 'update'], 846 'nics.*.interface': ['add', 'update'], 847 'nics.*.ip': ['add', 'update'], 848 'nics.*.mac': ['add', 'update'], 849 'nics.*.netmask': ['add', 'update'], 850 'nics.*.network_uuid': ['add', 'update'], 851 'nics.*.nic_tag': ['add', 'update'], 852 'nics.*.vrrp_vrid': ['add', 'update'], 853 'nics.*.vrrp_primary_ip': ['add', 'update'], 854 'nics.*.primary': ['add', 'update'], 855 'nics.*.vlan_id': ['add', 'update'], 856 'nowait': ['create', 'receive'], 857 'owner_uuid': ['create', 'receive', 'update'], 858 'package_name': ['create', 'receive', 'update'], 859 'package_version': ['create', 'receive', 'update'], 860 'quota': ['create', 'receive', 'update'], 861 'ram': ['create', 'receive', 'update'], 862 'remove_customer_metadata': ['update'], 863 'remove_internal_metadata': ['update'], 864 'remove_nics': ['update'], 865 'remove_routes': ['update'], 866 'remove_tags': ['update'], 867 'restart_init': ['create', 'receive', 'update'], 868 'resolvers': ['create', 'receive', 'update'], 869 'routes': ['create', 'receive'], 870 'set_customer_metadata': ['update'], 871 'set_internal_metadata': ['update'], 872 'set_routes': ['update'], 873 'set_tags': ['update'], 874 'tags': ['create', 'receive'], 875 'tmpfs': ['create', 'receive', 'update'], 876 'transition': ['receive'], 877 'update_nics': ['update'], 878 'uuid': ['create', 'receive'], 879 'v': ['receive'], 880 'zfs_data_compression': ['create', 'receive', 'update'], 881 'zfs_data_recsize': ['create', 'receive', 'update'], 882 'zfs_io_priority': ['create', 'receive', 'update'], 883 'zfs_root_compression': ['create', 'receive', 'update'], 884 'zfs_root_recsize': ['create', 'receive', 'update'], 885 'zfs_storage_pool_name': ['create', 'receive'], 886 'zonename': ['create', 'receive'], 887 'zpool': ['create', 'receive'] 888 }; 889 890 /* 891 * This defines all of the properties allowed, required and features that a 892 * brand has. For each of the allowed/required properties you have a list of 893 * actions for which this is allowed/required. For properties that are lists 894 * of objects, you can specify the action as 'add' or 'update' for when you're 895 * adding or updating one of those objects. 896 * 897 * Features can currently be one of: 898 * 899 * 'cleanup_dataset' -- (boolean) whether to remove trash before booting 900 * 'default_memory_overhead' -- (integer) memory above 'ram' that's added 901 * 'limit_priv': (list) list of priviledges for this zone (if not 'default') 902 * 'mdata_restart' -- (boolean) whether the brand supports restarting its 903 * mdata:fetch service to update properties in the zone 904 * 'min_memory_overhead' -- (integer) minimum delta between ram + max_physical 905 * 'model_required' -- (boolean) whether a .model is required on nics and disks 906 * 'pid_file' -- (pathname) file containing the PID for zones with one process 907 * 'runtime_info' -- (boolean) whether this zone supports the 'info' command 908 * 'serial_console' -- (boolean) whether this zone uses serial console 909 * 'type' -- the type of the VM (OS or KVM), all brands should include this 910 * 'update_mdata_exec_timeout' (boolean) whether to update mdata:exec timeout 911 * 'update_rctls' (boolean) whether we can update rctls 'live' for this zone 912 * 'use_tmpfs' -- (boolean) whether this type of zone uses tmpfs 913 * 'use_vm_autoboot' -- (boolean) use vm-autoboot instead of autoboot 914 * 'use_vmadmd' -- (boolean) use vmadmd for some actions instead of direct 915 * 'var_svc_provisioning' -- (boolean) whether brand uses /var/svc/provisioning 916 * 'wait_for_hwsetup' -- (boolean) use QMP and provision_success when hwsetup 917 * 'write_zone_netfiles' -- (boolean) write out files like /etc/hostname.net0 918 * 'zlogin_console' -- (boolean) use zlogin -C for console (vs. serial_console) 919 * 'zoneinit' -- (boolean) this brand's setup may be controlled by zoneinit 920 * 921 * All of the keys: 922 * 923 * allowed_properties 924 * required_properties 925 * features 926 * 927 * should be defined for each brand. Even if empty. 928 */ 929 var BRAND_OPTIONS = { 930 'joyent': { 931 'allowed_properties': joyent_allowed, 932 'required_properties': { 933 'brand': ['create', 'receive'], 934 'image_uuid': ['create', 'receive'] 935 }, 'features': { 936 'brand_install_script': '/usr/lib/brand/joyent/jinstall', 937 'cleanup_dataset': true, 938 'mdata_restart': true, 939 'reprovision': true, 940 'type': 'OS', 941 'update_mdata_exec_timeout': true, 942 'update_rctls': true, 943 'use_tmpfs': true, 944 'write_zone_netfiles': true, 945 'zlogin_console': true, 946 'zoneinit': true 947 } 948 }, 'joyent-minimal': { 949 'allowed_properties': joyent_allowed, 950 'required_properties': { 951 'brand': ['create', 'receive'], 952 'image_uuid': ['create', 'receive'] 953 }, 'features': { 954 'brand_install_script': '/usr/lib/brand/joyent-minimal/jinstall', 955 'cleanup_dataset': true, 956 'mdata_restart': true, 957 'reprovision': true, 958 'type': 'OS', 959 'update_mdata_exec_timeout': true, 960 'update_rctls': true, 961 'use_tmpfs': true, 962 'var_svc_provisioning': true, 963 'write_zone_netfiles': true, 964 'zlogin_console': true 965 } 966 }, 'sngl': { 967 'allowed_properties': joyent_allowed, 968 'required_properties': { 969 'brand': ['create', 'receive'], 970 'image_uuid': ['create', 'receive'] 971 }, 'features': { 972 'cleanup_dataset': true, 973 'mdata_restart': true, 974 'type': 'OS', 975 'update_mdata_exec_timeout': true, 976 'update_rctls': true, 977 'use_tmpfs': true, 978 'write_zone_netfiles': true, 979 'zlogin_console': true, 980 'zoneinit': true 981 } 982 }, 'kvm': { 983 'allowed_properties': { 984 'add_disks': ['update'], 985 'add_nics': ['update'], 986 'alias': ['create', 'receive', 'update'], 987 'archive_on_delete': ['create', 'receive', 'update'], 988 'autoboot': ['create', 'receive', 'update'], 989 'billing_id': ['create', 'receive', 'update'], 990 'boot': ['create', 'receive', 'update'], 991 'brand': ['create', 'receive'], 992 'cpu_cap': ['create', 'receive', 'update'], 993 'cpu_shares': ['create', 'receive', 'update'], 994 'cpu_type': ['create', 'receive', 'update'], 995 'create_only': ['receive'], 996 'create_timestamp': ['receive'], 997 'customer_metadata': ['create', 'receive'], 998 'disks': ['create', 'receive'], 999 'disks.*.block_size': ['add'], 1000 'disks.*.boot': ['add', 'update'], 1001 'disks.*.compression': ['add', 'update'], 1002 'disks.*.image_name': ['add', 'update'], 1003 'disks.*.image_size': ['add'], 1004 'disks.*.image_uuid': ['add'], 1005 'disks.*.refreservation': ['add', 'update'], 1006 'disks.*.size': ['add'], 1007 'disks.*.media': ['add', 'update'], 1008 'disks.*.model': ['add', 'update'], 1009 'disks.*.nocreate': ['add'], 1010 'disks.*.path': ['add', 'update'], 1011 'disks.*.zpool': ['add'], 1012 'disk_driver': ['create', 'receive', 'update'], 1013 'do_not_inventory': ['create', 'receive', 'update'], 1014 'firewall': ['create'], 1015 'firewall_enabled': ['create', 'receive', 'update'], 1016 'hostname': ['create', 'receive', 'update'], 1017 'image_uuid': ['create', 'receive'], 1018 'internal_metadata': ['create', 'receive'], 1019 'limit_priv': ['create', 'receive', 'update'], 1020 'max_locked_memory': ['create', 'receive', 'update'], 1021 'max_lwps': ['create', 'receive', 'update'], 1022 'max_physical_memory': ['create', 'receive', 'update'], 1023 'max_swap': ['create', 'receive', 'update'], 1024 'nics': ['create', 'receive'], 1025 'nics.*.allow_dhcp_spoofing': ['add', 'update'], 1026 'nics.*.allow_ip_spoofing': ['add', 'update'], 1027 'nics.*.allow_mac_spoofing': ['add', 'update'], 1028 'nics.*.allow_restricted_traffic': ['add', 'update'], 1029 'nics.*.allow_unfiltered_promisc': ['add', 'update'], 1030 'nics.*.allowed_ips': ['add', 'update'], 1031 'nics.*.blocked_outgoing_ports': ['add', 'update'], 1032 'nics.*.dhcp_server': ['add', 'update'], 1033 'nics.*.gateway': ['add', 'update'], 1034 'nics.*.interface': ['add', 'update'], 1035 'nics.*.ip': ['add', 'update'], 1036 'nics.*.mac': ['add', 'update'], 1037 'nics.*.model': ['add', 'update'], 1038 'nics.*.netmask': ['add', 'update'], 1039 'nics.*.network_uuid': ['add', 'update'], 1040 'nics.*.nic_tag': ['add', 'update'], 1041 'nics.*.primary': ['add', 'update'], 1042 'nics.*.vlan_id': ['add', 'update'], 1043 'nic_driver': ['create', 'receive', 'update'], 1044 'owner_uuid': ['create', 'receive', 'update'], 1045 'package_name': ['create', 'receive', 'update'], 1046 'package_version': ['create', 'receive', 'update'], 1047 'qemu_opts': ['create', 'receive', 'update'], 1048 'qemu_extra_opts': ['create', 'receive', 'update'], 1049 'quota': ['create', 'receive', 'update'], 1050 'ram': ['create', 'receive', 'update'], 1051 'remove_customer_metadata': ['update'], 1052 'remove_disks': ['update'], 1053 'remove_internal_metadata': ['update'], 1054 'remove_nics': ['update'], 1055 'remove_routes': ['update'], 1056 'remove_tags': ['update'], 1057 'resolvers': ['create', 'receive', 'update'], 1058 'set_customer_metadata': ['update'], 1059 'set_internal_metadata': ['update'], 1060 'set_routes': ['update'], 1061 'set_tags': ['update'], 1062 'spice_opts': ['create', 'receive', 'update'], 1063 'spice_password': ['create', 'receive', 'update'], 1064 'spice_port': ['create', 'receive', 'update'], 1065 'tags': ['create', 'receive'], 1066 'transition': ['receive'], 1067 'update_disks': ['update'], 1068 'update_nics': ['update'], 1069 'uuid': ['create', 'receive'], 1070 'v': ['receive'], 1071 'vcpus': ['create', 'receive', 'update'], 1072 'vga': ['create', 'receive', 'update'], 1073 'virtio_txburst': ['create', 'receive', 'update'], 1074 'virtio_txtimer': ['create', 'receive', 'update'], 1075 'vnc_password': ['create', 'receive', 'update'], 1076 'vnc_port': ['create', 'receive', 'update'], 1077 'zfs_io_priority': ['create', 'receive', 'update'], 1078 'zfs_root_compression': ['create', 'receive', 'update'], 1079 'zfs_root_recsize': ['create', 'receive', 'update'], 1080 'zone_dataset_uuid': ['create', 'receive'], 1081 'zpool': ['create', 'receive'] 1082 }, 'required_properties': { 1083 'brand': ['create', 'receive'] 1084 }, 'features': { 1085 'default_memory_overhead': VM.KVM_MEM_OVERHEAD, 1086 'limit_priv': ['default', '-file_link_any', '-net_access', 1087 '-proc_fork', '-proc_info', '-proc_session'], 1088 'min_memory_overhead': VM.KVM_MIN_MEM_OVERHEAD, 1089 'model_required': true, 1090 'pid_file': '/tmp/vm.pid', 1091 'runtime_info': true, 1092 'serial_console': true, 1093 'type': 'KVM', 1094 'use_vm_autoboot': true, 1095 'use_vmadmd': true, 1096 'var_svc_provisioning': true, 1097 'wait_for_hwsetup': true 1098 } 1099 } 1100 }; 1101 1102 var VIRTIO_TXTIMER_DEFAULT = 200000; 1103 var VIRTIO_TXBURST_DEFAULT = 128; 1104 1105 function getZpools(log, callback) 1106 { 1107 var args = ['list', '-H', '-p', '-o', 'name']; 1108 var cmd = '/usr/sbin/zpool'; 1109 var idx; 1110 var raw = []; 1111 var zpools = []; 1112 1113 assert(log, 'no logger passed to getZpools()'); 1114 1115 log.debug(cmd + ' ' + args.join(' ')); 1116 execFile(cmd, args, function (error, stdout, stderr) { 1117 if (error) { 1118 log.error('Unable to get list of zpools'); 1119 callback(error, {'stdout': stdout, 'stderr': stderr}); 1120 } else { 1121 // strip out any empty values (last one). 1122 raw = stdout.split('\n'); 1123 for (idx in raw) { 1124 if (raw[idx].length > 0) { 1125 zpools.push(raw[idx]); 1126 } 1127 } 1128 callback(null, zpools); 1129 } 1130 }); 1131 } 1132 1133 /* 1134 * When you need to access files inside a zoneroot, you need to be careful that 1135 * there are no symlinks in the path. Since we operate from the GZ, these 1136 * symlinks will be evaluated in the GZ context. Eg. a symlink in zone A with 1137 * /var/run -> * /zones/<uuid of zone B>/root/var/run would mean that operating 1138 * on files in zone A's /var/run would actually be touching files in zone B. 1139 * 1140 * To prevent that, only ever modify files inside the zoneroot from the GZ 1141 * *before* first boot. After the zone is booted, it's better to use services 1142 * in the zone to pull values from metadata and write out changes on next boot. 1143 * It's also safe to use zlogin when the zone is running. 1144 * 1145 * This function is intended to be used in those cases we do write things out 1146 * before the zone's first boot but the dataset might have invalid symlinks in 1147 * it even then, so we still need to confirm the paths inside zoneroot before 1148 * using them. It throws an exception if: 1149 * 1150 * - zoneroot is not an absolute path 1151 * - fs.lstatSync fails 1152 * - target path under zoneroot contains symlink 1153 * - a component leading up to the final one is not a directory 1154 * - options.type is set to 'file' and target is not a regular file 1155 * - options.type is set to 'dir' and target references a non-directory 1156 * - options.type is not one of 'file' or 'dir' 1157 * - options.enoent_ok is false and target path doesn't exist 1158 * 1159 * if none of those are the case, it returns true. 1160 */ 1161 function assertSafeZonePath(zoneroot, target, options) 1162 { 1163 var parts; 1164 var root; 1165 var stat; 1166 var test; 1167 1168 assert((zoneroot.length > 0 && zoneroot[0] === '/'), 1169 'zoneroot must be an absolute path not: [' + zoneroot + ']'); 1170 1171 parts = trim(target, '/').split('/'); 1172 root = trim(zoneroot, '/'); 1173 test = '/' + root; 1174 1175 while (parts.length > 0) { 1176 test = test + '/' + parts.shift(); 1177 1178 try { 1179 stat = fs.lstatSync(test); 1180 } catch (e) { 1181 if (e.code === 'ENOENT') { 1182 if (!options.hasOwnProperty('enoent_ok') 1183 || options.enoent_ok === false) { 1184 1185 throw e; 1186 } else { 1187 // enoent is ok, return true. This is mostly used when 1188 // deleting files with rm -f <path>. It's ok for <path> to 1189 // not exist (but not ok for any component to be a symlink) 1190 // there's no point continuing though since ENOENT here 1191 // means all subpaths also won't exist. 1192 return true; 1193 } 1194 } else { 1195 throw e; 1196 } 1197 } 1198 1199 if (stat.isSymbolicLink()) { 1200 // it's never ok to have a symlink component 1201 throw new Error(test + ' is a symlink'); 1202 } 1203 1204 // any component other than the last also needs to be a 1205 // directory, last can also be a file. 1206 if (parts.length === 0) { 1207 // last, dir or file 1208 if (!options.hasOwnProperty('type') || options.type === 'dir') { 1209 if (!stat.isDirectory()) { 1210 throw new Error(test + ' is not a directory'); 1211 } 1212 } else if (options.type === 'file') { 1213 if (!stat.isFile()) { 1214 throw new Error(test + ' is not a file'); 1215 } 1216 } else { 1217 throw new Error('this function does not know about ' 1218 + options.type); 1219 } 1220 } else if (!stat.isDirectory()) { 1221 // not last component, only dir is acceptable 1222 throw new Error(test + ' is not a directory'); 1223 } 1224 } 1225 // if we didn't throw, this is valid. 1226 return true; 1227 } 1228 1229 function validateProperty(brand, prop, value, action, data, errors, log) 1230 { 1231 var allowed; 1232 var k; 1233 1234 assert(log, 'no logger passed to validateProperty()'); 1235 1236 if (!data.hasOwnProperty('zpools')) { 1237 data.zpools = []; 1238 } 1239 1240 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')) { 1241 allowed = BRAND_OPTIONS[brand].allowed_properties; 1242 } else { 1243 allowed = {}; 1244 } 1245 1246 if (!errors.hasOwnProperty('bad_values')) { 1247 errors.bad_values = []; 1248 } 1249 if (!errors.hasOwnProperty('bad_properties')) { 1250 errors.bad_properties = []; 1251 } 1252 1253 if (!allowed.hasOwnProperty(prop)) { 1254 // thie BRAND_OPTIONS doesn't have this property at all 1255 if (errors.bad_properties.indexOf(prop) === -1) { 1256 errors.bad_properties.push(prop); 1257 } 1258 } else if (!Array.isArray(allowed[prop]) 1259 || allowed[prop].indexOf(action) === -1) { 1260 1261 // here we've ether got no actions allowed for this value, 1262 // or just not this one 1263 if (errors.bad_properties.indexOf(prop) === -1) { 1264 errors.bad_properties.push(prop); 1265 } 1266 } 1267 1268 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)) { 1269 switch (PAYLOAD_PROPERTIES[prop].type) { 1270 case 'uuid': 1271 if (typeof (value) === 'string' && !isUUID(value) 1272 && errors.bad_values.indexOf(prop) === -1) { 1273 1274 errors.bad_values.push(prop); 1275 } 1276 break; 1277 case 'boolean': 1278 if (value === 1 || value === '1') { 1279 log.warn('DEPRECATED: payload uses 1 instead of ' 1280 + 'true for ' + prop + ', use "true" instead.'); 1281 } else if (typeof (fixBoolean(value)) !== 'boolean' 1282 && errors.bad_values.indexOf(prop) === -1) { 1283 1284 errors.bad_values.push(prop); 1285 } 1286 break; 1287 case 'string': 1288 if (value === undefined || value === null 1289 || trim(value.toString()) === '') { 1290 // if set empty/false we'll keep since this is used to unset 1291 break; 1292 } else if (typeof (value) !== 'string' 1293 && errors.bad_values.indexOf(prop) === -1) { 1294 1295 errors.bad_values.push(prop); 1296 } 1297 break; 1298 case 'integer': 1299 if (value === undefined || value === null 1300 || trim(value.toString()) === '') { 1301 // if set empty/false we'll keep since this is used to unset 1302 break; 1303 } else if (((typeof (value) !== 'string' 1304 && typeof (value) !== 'number') 1305 || !value.toString().match(/^[0-9]+$/)) 1306 && errors.bad_values.indexOf(prop) === -1) { 1307 1308 if ((['vnc_port', 'spice_port'].indexOf(prop) !== -1) 1309 && (value.toString() === '-1')) { 1310 1311 // these keys allow '-1' as a value, so we succeed here even 1312 // though we'd otherwise fail. 1313 break; 1314 } 1315 1316 errors.bad_values.push(prop); 1317 } else if (prop === 'max_swap' && value < MINIMUM_MAX_SWAP) { 1318 errors.bad_values.push(prop); 1319 } 1320 break; 1321 case 'integer-8bit': 1322 if (value === undefined || value === null 1323 || trim(value.toString()) === '') { 1324 // if set empty/false we'll keep since this is used to unset 1325 break; 1326 } else if (((typeof (value) !== 'string' 1327 && typeof (value) !== 'number') 1328 || !value.toString().match(/^[0-9]+$/)) 1329 && errors.bad_values.indexOf(prop) === -1 1330 ) { 1331 1332 errors.bad_values.push(prop); 1333 break; 1334 } 1335 if (value < 0 || value > 255) { 1336 errors.bad_values.push(prop); 1337 } 1338 break; 1339 case 'zpool': 1340 if ((typeof (value) !== 'string' 1341 || data.zpools.indexOf(value) === -1) 1342 && errors.bad_values.indexOf(prop) === -1) { 1343 1344 errors.bad_values.push(prop); 1345 } 1346 break; 1347 case 'object': 1348 if (typeof (value) !== 'object' 1349 && errors.bad_values.indexOf(prop) === -1) { 1350 1351 errors.bad_values.push(prop); 1352 } 1353 break; 1354 case 'flat-object': 1355 if (typeof (value) !== 'object' 1356 && errors.bad_values.indexOf(prop) === -1) { 1357 1358 errors.bad_values.push(prop); 1359 } 1360 for (k in value) { 1361 if (typeof (value[k]) !== 'string' 1362 && typeof (value[k]) !== 'number' 1363 && typeof (value[k]) !== 'boolean') { 1364 1365 if (errors.bad_values.indexOf(prop) === -1) { 1366 errors.bad_values.push(prop); 1367 } 1368 break; 1369 } 1370 } 1371 break; 1372 case 'list': 1373 if (typeof (value) === 'string') { 1374 // really any string could be valid (a one element list) 1375 break; 1376 } else if (Array.isArray(value)) { 1377 for (k in value) { 1378 if (typeof (value[k]) !== 'string' 1379 && typeof (value[k]) !== 'number') { 1380 1381 // TODO: log something more useful here telling them 1382 // the type is invalid. 1383 if (errors.bad_values.indexOf(prop) === -1) { 1384 errors.bad_values.push(prop); 1385 } 1386 break; 1387 } 1388 // if this is an array, it can't have commas in the 1389 // values. (since we might stringify the list and 1390 // we'd end up with something different. 1391 if (value[k].toString().indexOf(',') !== -1 1392 && errors.bad_values.indexOf(prop) === -1) { 1393 1394 errors.bad_values.push(prop); 1395 } 1396 } 1397 } else { 1398 // not a valid type 1399 if (errors.bad_values.indexOf(prop) === -1) { 1400 errors.bad_values.push(prop); 1401 } 1402 } 1403 break; 1404 case 'object-array': 1405 if (!Array.isArray(value)) { 1406 if (errors.bad_values.indexOf(prop) === -1) { 1407 errors.bad_values.push(prop); 1408 } 1409 break; 1410 } 1411 for (k in value) { 1412 if (typeof (value[k]) !== 'object') { 1413 if (errors.bad_values.indexOf(prop) === -1) { 1414 errors.bad_values.push(prop); 1415 } 1416 break; 1417 } 1418 } 1419 break; 1420 default: 1421 // don't know what type of prop this is, so it's invalid 1422 if (errors.bad_properties.indexOf(prop) === -1) { 1423 errors.bad_properties.push(prop); 1424 } 1425 break; 1426 } 1427 } 1428 } 1429 1430 /* 1431 * image properties: 1432 * 1433 * size (optional, only used by zvols) 1434 * type ('zvol' or 'zone-dataset') 1435 * uuid 1436 * zpool 1437 * 1438 */ 1439 function validateImage(image, log, callback) 1440 { 1441 var args; 1442 var cmd = '/usr/sbin/imgadm'; 1443 1444 args = ['get', '-P', image.zpool, image.uuid]; 1445 1446 log.debug(cmd + ' ' + args.join(' ')); 1447 1448 // on any error we fail closed (assume the image does not exist) 1449 execFile(cmd, args, function (error, stdout, stderr) { 1450 var data; 1451 var e; 1452 1453 if (error) { 1454 error.stdout = stdout; 1455 error.stderr = stderr; 1456 error.whatFailed = 'EEXECFILE'; 1457 log.error(error); 1458 callback(error); 1459 return; 1460 } 1461 1462 try { 1463 data = JSON.parse(stdout.toString()); 1464 } catch (err) { 1465 data = {}; 1466 } 1467 1468 if (data.hasOwnProperty('manifest')) { 1469 if (data.manifest.type !== image.type) { 1470 // image is wrong type 1471 e = new Error('image ' + image.uuid + ' is type ' 1472 + data.manifest.type + ', must be ' + image.type); 1473 e.whatFailed = 'EBADTYPE'; 1474 log.error(e); 1475 callback(e); 1476 return; 1477 } 1478 log.info('image ' + image.uuid + ' found in imgadm'); 1479 1480 // If image_size is missing, add it. If it's wrong, error. 1481 if (data.manifest.hasOwnProperty('image_size')) { 1482 if (image.hasOwnProperty('size')) { 1483 if (image.size !== data.manifest.image_size) { 1484 e = new Error('incorrect image_size value for image' 1485 + ' ' + image.uuid + ' passed: ' 1486 + image.size + ' should be: ' 1487 + data.manifest.image_size); 1488 e.whatFailed = 'EBADSIZE'; 1489 log.error(e); 1490 callback(e); 1491 return; 1492 } 1493 } else { 1494 // image doesn't have size, manifest does, add it. 1495 image.size = data.manifest.image_size; 1496 } 1497 } 1498 // everything ok 1499 callback(); 1500 } else { 1501 e = new Error('cannot find \'manifest\' for image ' 1502 + image.uuid); 1503 e.whatFailed = 'ENOENT'; 1504 log.error(e); 1505 callback(e); 1506 return; 1507 } 1508 }); 1509 } 1510 1511 // Ensure if image_uuid is passed either at top level or for disks.*.image_uuid 1512 // that image_uuid exists on the system according to imgadm. 1513 // 1514 // NOTE: if image_size is missing from payload, but found in imgadm it is added 1515 // to the payload here. 1516 // 1517 function validateImages(payload, errors, log, callback) 1518 { 1519 var check_images = []; 1520 var disk_idx; 1521 var pool; 1522 1523 if (payload.hasOwnProperty('image_uuid') && isUUID(payload.image_uuid)) { 1524 if (payload.hasOwnProperty('zpool')) { 1525 pool = payload.zpool; 1526 } else { 1527 pool = 'zones'; 1528 } 1529 1530 check_images.push({ 1531 'property': 'image_uuid', 1532 'target': payload, 1533 'type': 'zone-dataset', 1534 'uuid': payload.image_uuid, 1535 'zpool': pool 1536 }); 1537 } 1538 1539 ['disks', 'add_disks'].forEach(function (d) { 1540 if (payload.hasOwnProperty(d)) { 1541 disk_idx = 0; 1542 payload[d].forEach(function (disk) { 1543 if (disk.hasOwnProperty('image_uuid')) { 1544 if (disk.hasOwnProperty('zpool')) { 1545 pool = disk.zpool; 1546 } else { 1547 pool = 'zones'; 1548 } 1549 check_images.push({ 1550 'property_prefix': d + '.' + disk_idx, 1551 'property': d + '.' + disk_idx + '.image_uuid', 1552 'target': disk, 1553 'type': 'zvol', 1554 'uuid': disk.image_uuid, 1555 'zpool': pool 1556 }); 1557 } 1558 disk_idx++; 1559 }); 1560 } 1561 }); 1562 1563 async.forEachSeries(check_images, function (image, cb) { 1564 1565 var i; 1566 var idx; 1567 1568 i = { 1569 uuid: image.uuid, 1570 type: image.type, 1571 zpool: image.zpool 1572 }; 1573 1574 if (image.target.hasOwnProperty('image_size')) { 1575 i.size = image.target.image_size; 1576 } 1577 1578 validateImage(i, log, function (err) { 1579 if (err) { 1580 switch (err.whatFailed) { 1581 case 'EBADSIZE': 1582 // image.size is wrong (vs. manifest) 1583 errors.bad_values.push(image.property_prefix 1584 + '.image_size'); 1585 break; 1586 case 'ENOENT': 1587 // image.uuid not found in imgadm 1588 errors.bad_values.push(image.property); 1589 break; 1590 case 'EBADTYPE': 1591 // image.type is wrong 1592 errors.bad_values.push(image.property); 1593 break; 1594 default: 1595 // unknown error, fail closed 1596 errors.bad_values.push(image.property); 1597 break; 1598 } 1599 } else { 1600 // no errors, so check if size was added 1601 if (i.hasOwnProperty('size')) { 1602 if (!image.target.hasOwnProperty('image_size')) { 1603 image.target.image_size = i.size; 1604 // Remove error that would have been added earlier 1605 // when we didn't have image_size 1606 idx = errors.missing_properties.indexOf( 1607 image.property_prefix + '.image_size'); 1608 if (idx !== -1) { 1609 errors.missing_properties.splice(idx, 1); 1610 } 1611 } 1612 } 1613 } 1614 1615 cb(); 1616 }); 1617 }, function () { 1618 callback(); 1619 }); 1620 } 1621 1622 // This is for allowed_ips which accepts IPiv4 addresses or CIDR addresses in 1623 // the form IP/MASK where MASK is 1-32. 1624 function validateIPlist(list) { 1625 var invalid = []; 1626 1627 list.forEach(function (ip) { 1628 var matches; 1629 if (!net.isIPv4(ip)) { 1630 matches = ip.match(/^([0-9\.]+)\/([0-9]+)$/); 1631 if (matches && net.isIPv4(matches[1]) 1632 && (Number(matches[2]) >= 1) && (Number(matches[2]) <= 32)) { 1633 1634 // In this case it wasn't an IPv4, but it was a valid CIDR 1635 return; 1636 } else { 1637 invalid.push(ip); 1638 } 1639 } 1640 }); 1641 1642 if (invalid.length !== 0) { 1643 throw new Error('invalid allowed_ips: ' + invalid.join(', ')); 1644 } 1645 1646 if (list.length > 13) { 1647 throw new Error('Maximum of 13 allowed_ips per nic'); 1648 } 1649 } 1650 1651 exports.validate = function (brand, action, payload, options, callback) 1652 { 1653 var errors = { 1654 'bad_values': [], 1655 'bad_properties': [], 1656 'missing_properties': [] 1657 }; 1658 var log; 1659 var prop; 1660 1661 // options is optional 1662 if (arguments.length === 4) { 1663 callback = arguments[3]; 1664 options = {}; 1665 } 1666 1667 ensureLogging(false); 1668 if (options.hasOwnProperty('log')) { 1669 log = options.log; 1670 } else { 1671 log = VM.log.child({action: 'validate'}); 1672 } 1673 1674 if (!BRAND_OPTIONS.hasOwnProperty(brand)) { 1675 if (!brand) { 1676 brand = 'undefined'; 1677 } 1678 callback({'bad_brand': brand}); 1679 return; 1680 } 1681 1682 // wrap the whole thing with getZpools so we have the list of pools if we 1683 // need them. 1684 getZpools(log, function (err, zpools) { 1685 var disk_idx; 1686 var idx; 1687 var prefix; 1688 var required; 1689 var subprop; 1690 var subprop_action = ''; 1691 var value; 1692 1693 if (err) { 1694 /* 1695 * this only happens when the zpool command fails which should be 1696 * very rare, but when it does happen, we continue with an empty 1697 * zpool list in case they don't need to validate zpools. If they 1698 * do, every zpool will be invalid which is also what we want since 1699 * nothing else that uses zpools is likely to work either. 1700 * 1701 */ 1702 zpools = []; 1703 } 1704 1705 // loop through and weed out ones we don't allow for this action. 1706 for (prop in payload) { 1707 validateProperty(brand, prop, payload[prop], action, 1708 {zpools: zpools}, errors, log); 1709 1710 // special case for complex properties where we want to check 1711 // foo.*.whatever 1712 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop) 1713 && PAYLOAD_PROPERTIES[prop].type === 'object-array' 1714 && Array.isArray(payload[prop])) { 1715 1716 if (PAYLOAD_PROPERTIES[prop].hasOwnProperty('check_as')) { 1717 prefix = PAYLOAD_PROPERTIES[prop].check_as + '.*.'; 1718 if (prop.match(/^add_/)) { 1719 subprop_action = 'add'; 1720 } else if (prop.match(/^update_/)) { 1721 subprop_action = 'update'; 1722 } 1723 } else { 1724 // here we've got something like 'disks' which is an add 1725 prefix = prop + '.*.'; 1726 subprop_action = 'add'; 1727 } 1728 1729 for (idx in payload[prop]) { 1730 if (typeof (payload[prop][idx]) === 'object') { 1731 // subprop will be something like 'nic_tag' 1732 for (subprop in payload[prop][idx]) { 1733 value = payload[prop][idx][subprop]; 1734 validateProperty(brand, prefix + subprop, value, 1735 subprop_action, {zpools: zpools}, errors, log); 1736 } 1737 } else if (errors.bad_values.indexOf(prop) === -1) { 1738 // this is not an object so bad value in the array 1739 errors.bad_values.push(prop); 1740 } 1741 } 1742 } 1743 } 1744 1745 // special case: if you have disks you must specify either image_uuid 1746 // and image_size *or* size and block_size is only allowed when you use 1747 // 'size' and image_name when you don't. 1748 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties') 1749 && BRAND_OPTIONS[brand].allowed_properties 1750 .hasOwnProperty('disks')) { 1751 1752 function validateDiskSource(prop_prefix, disk) { 1753 1754 if (disk.hasOwnProperty('media') && disk.media !== 'disk') { 1755 // we only care about disks here, not cdroms. 1756 return; 1757 } 1758 1759 if (disk.hasOwnProperty('image_uuid')) { 1760 // with image_uuid, size is invalid and image_size is 1761 // required, additionally block_size is not allowed. 1762 1763 if (!disk.hasOwnProperty('image_size')) { 1764 errors.missing_properties.push(prop_prefix 1765 + '.image_size'); 1766 } 1767 if (disk.hasOwnProperty('size')) { 1768 errors.bad_properties.push(prop_prefix + '.size'); 1769 } 1770 if (disk.hasOwnProperty('block_size')) { 1771 errors.bad_properties.push(prop_prefix 1772 + '.block_size'); 1773 } 1774 } else { 1775 // without image_uuid, image_size and image_name are invalid 1776 // and 'size' is required. 1777 1778 if (!disk.hasOwnProperty('size')) { 1779 errors.missing_properties.push(prop_prefix + '.size'); 1780 } 1781 if (disk.hasOwnProperty('image_name')) { 1782 errors.bad_properties.push(prop_prefix + '.image_name'); 1783 } 1784 if (disk.hasOwnProperty('image_size')) { 1785 errors.bad_properties.push(prop_prefix + '.image_size'); 1786 } 1787 } 1788 } 1789 1790 if (payload.hasOwnProperty('disks')) { 1791 for (disk_idx in payload.disks) { 1792 validateDiskSource('disks.' + disk_idx, 1793 payload.disks[disk_idx]); 1794 } 1795 } 1796 if (payload.hasOwnProperty('add_disks')) { 1797 for (disk_idx in payload.add_disks) { 1798 validateDiskSource('add_disks.' + disk_idx, 1799 payload.add_disks[disk_idx]); 1800 } 1801 } 1802 } 1803 1804 if (BRAND_OPTIONS[brand].hasOwnProperty('required_properties')) { 1805 required = BRAND_OPTIONS[brand].required_properties; 1806 for (prop in required) { 1807 if (required[prop].indexOf(action) !== -1 1808 && !payload.hasOwnProperty(prop)) { 1809 1810 errors.missing_properties.push(prop); 1811 } 1812 } 1813 } 1814 1815 // make sure any images in the payload are also valid 1816 // NOTE: if validateImages() finds errors, it adds to 'errors' here. 1817 validateImages(payload, errors, log, function () { 1818 1819 // we validate disks.*.refreservation here because image_size might 1820 // not be populated yet until we return from validateImages() 1821 ['disks', 'add_disks'].forEach(function (d) { 1822 var d_idx = 0; 1823 if (payload.hasOwnProperty(d)) { 1824 payload[d].forEach(function (disk) { 1825 if (disk.hasOwnProperty('refreservation')) { 1826 if (disk.refreservation < 0) { 1827 errors.bad_values.push(d + '.' + d_idx 1828 + '.refreservation'); 1829 } else if (disk.size 1830 && disk.refreservation > disk.size) { 1831 1832 errors.bad_values.push(d + '.' + d_idx 1833 + '.refreservation'); 1834 } else if (disk.image_size 1835 && disk.refreservation > disk.image_size) { 1836 1837 errors.bad_values.push(d + '.' + d_idx 1838 + '.refreservation'); 1839 } 1840 } 1841 d_idx++; 1842 }); 1843 } 1844 }); 1845 1846 if (errors.bad_properties.length > 0 || errors.bad_values.length > 0 1847 || errors.missing_properties.length > 0) { 1848 1849 callback(errors); 1850 return; 1851 } 1852 1853 callback(); 1854 }); 1855 }); 1856 }; 1857 1858 function separateCommas(str) 1859 { 1860 return str.split(','); 1861 } 1862 1863 function unmangleMem(str) 1864 { 1865 return (Number(str) / (1024 * 1024)); 1866 } 1867 1868 function unbase64(str) 1869 { 1870 return new Buffer(str, 'base64').toString('ascii'); 1871 } 1872 1873 function numberify(str) 1874 { 1875 return Number(str); 1876 } 1877 1878 function startElement(name, attrs, state, log) { 1879 var disk = {}; 1880 var key; 1881 var newobj; 1882 var nic = {}; 1883 var obj; 1884 var prop; 1885 var stack; 1886 var use; 1887 var where; 1888 1889 assert(log, 'no logger passed to startElement()'); 1890 1891 if (!state.hasOwnProperty('stack')) { 1892 state.stack = []; 1893 } 1894 obj = state.obj; 1895 stack = state.stack; 1896 1897 stack.push(name); 1898 where = stack.join('.'); 1899 1900 if (XML_PROPERTIES.hasOwnProperty(where)) { 1901 for (key in XML_PROPERTIES[where]) { 1902 use = XML_PROPERTIES[where][key]; 1903 if (attrs.hasOwnProperty(key)) { 1904 obj[use] = attrs[key]; 1905 } else if (attrs.hasOwnProperty('name') && attrs.name === key) { 1906 // attrs use the whacky {name, type, value} stuff. 1907 obj[use] = attrs['value']; 1908 } 1909 } 1910 } else if (where === 'zone.rctl') { 1911 stack.push(attrs.name); 1912 } else if (where === 'zone.network') { 1913 // new network device 1914 for (prop in attrs) { 1915 if (XML_PROPERTIES.nic.hasOwnProperty(prop)) { 1916 use = XML_PROPERTIES.nic[prop]; 1917 if (prop === 'mac-addr') { 1918 // XXX SmartOS inherited the ridiculous MAC formatting from 1919 // Solaris where leading zeros are removed. We should 1920 // Fix that in the OS tools. 1921 nic[use] = fixMac(attrs[prop]); 1922 } else { 1923 nic[use] = attrs[prop]; 1924 } 1925 } else { 1926 log.debug('unknown net prop: ' + prop); 1927 } 1928 } 1929 if (!obj.hasOwnProperty('networks')) { 1930 obj.networks = {}; 1931 } 1932 obj.networks[nic.mac] = nic; 1933 stack.push(nic.mac); 1934 } else if (where.match(/zone\.network\...:..:..:..:..:..\.net-attr/)) { 1935 if (XML_PROPERTIES.nic.hasOwnProperty(attrs.name)) { 1936 use = XML_PROPERTIES.nic[attrs.name]; 1937 obj.networks[stack[2]][use] = attrs.value; 1938 } else { 1939 log.debug('unknown net prop: ' + attrs.name); 1940 } 1941 } else if (where === 'zone.device') { 1942 // new disk device 1943 for (prop in attrs) { 1944 if (XML_PROPERTIES.disk.hasOwnProperty(prop)) { 1945 use = XML_PROPERTIES.disk[prop]; 1946 disk[use] = attrs[prop]; 1947 } else { 1948 log.debug('unknown disk prop: ' + prop); 1949 } 1950 } 1951 if (!obj.hasOwnProperty('devices')) { 1952 obj.devices = {}; 1953 } 1954 obj.devices[disk.path] = disk; 1955 stack.push(disk.path); 1956 } else if (where.match(/zone\.device\.\/.*\.net-attr/)) { 1957 if (XML_PROPERTIES.disk.hasOwnProperty(attrs.name)) { 1958 use = XML_PROPERTIES.disk[attrs.name]; 1959 obj.devices[stack[2]][use] = attrs.value; 1960 } else { 1961 log.debug('unknown disk prop: ' + attrs.name); 1962 } 1963 } else if (where === 'zone.dataset') { 1964 if (!obj.hasOwnProperty('datasets')) { 1965 obj.datasets = []; 1966 } 1967 if (attrs.hasOwnProperty('name')) { 1968 obj.datasets.push(attrs.name); 1969 } 1970 } else if (where === 'zone.filesystem') { 1971 if (!obj.hasOwnProperty('filesystems')) { 1972 obj.filesystems = []; 1973 } 1974 newobj = {}; 1975 for (prop in XML_PROPERTIES.filesystem) { 1976 if (attrs.hasOwnProperty(prop)) { 1977 newobj[XML_PROPERTIES.filesystem[prop]] = attrs[prop]; 1978 } 1979 } 1980 obj.filesystems.push(newobj); 1981 } else if (where === 'zone.filesystem.fsoption') { 1982 newobj = obj.filesystems.slice(-1)[0]; // the last element 1983 if (!newobj.hasOwnProperty('options')) { 1984 newobj.options = []; 1985 } 1986 newobj.options.push(attrs.name); 1987 } else { 1988 log.debug('unknown property: ' + where + ': ' 1989 + JSON.stringify(attrs)); 1990 } 1991 } 1992 1993 function endElement(name, state) { 1994 // trim stack back above this element 1995 var stack = state.stack; 1996 1997 while (stack.pop() !== name) { 1998 // do nothing, we just want to consume. 1999 continue; 2000 } 2001 } 2002 2003 function indexSort(obj, field, pattern) 2004 { 2005 obj.sort(function (a, b) { 2006 var avalue = 0; 2007 var bvalue = 0; 2008 var matches; 2009 2010 if (a.hasOwnProperty(field)) { 2011 matches = a[field].match(pattern); 2012 if (matches) { 2013 avalue = Number(matches[1]); 2014 } 2015 } 2016 if (b.hasOwnProperty(field)) { 2017 matches = b[field].match(pattern); 2018 if (matches) { 2019 bvalue = Number(matches[1]); 2020 } 2021 } 2022 2023 return avalue - bvalue; 2024 }); 2025 } 2026 2027 function applyTransforms(obj) 2028 { 2029 var p; 2030 var pp; 2031 var subobj; 2032 var transforms = XML_PROPERTY_TRANSFORMS; 2033 2034 for (p in transforms) { 2035 if (obj.hasOwnProperty(p)) { 2036 if (typeof (transforms[p]) === 'object') { 2037 // this is a 'complex' property like nic, and has different 2038 // transforms for the sub-objects 2039 for (pp in transforms[p]) { 2040 for (subobj in obj[p]) { 2041 if (obj[p][subobj].hasOwnProperty(pp)) { 2042 obj[p][subobj][pp] = 2043 transforms[p][pp](obj[p][subobj][pp]); 2044 } 2045 } 2046 } 2047 } else { // function 2048 obj[p] = transforms[p](obj[p]); 2049 } 2050 } 2051 } 2052 } 2053 2054 // This function parses the zone XML file at /etc/zones/<zonename>.xml and adds 2055 // the VM properties to a new object. 2056 function getVmobj(zonename, preload_data, options, callback) 2057 { 2058 var filename = '/etc/zones/' + zonename + '.xml'; 2059 var log; 2060 var parser = new expat.Parser('UTF-8'); 2061 2062 assert(options.log, 'no logger passed to getVmobj()'); 2063 log = options.log; 2064 2065 fs.readFile(filename, function (error, data) { 2066 var allowed; 2067 var disk; 2068 var dsinfo; 2069 var fields; 2070 var nic; 2071 var obj = {}; 2072 var state = {}; 2073 2074 if (error) { 2075 callback(error); 2076 return; 2077 } 2078 2079 state.obj = obj; 2080 parser.on('startElement', function (name, attrs) { 2081 return startElement(name, attrs, state, log); 2082 }); 2083 parser.on('endElement', function (name) { 2084 return endElement(name, state); 2085 }); 2086 2087 if (!parser.parse(data.toString())) { 2088 throw new Error('There are errors in your xml file: ' 2089 + parser.getError()); 2090 } 2091 2092 // now that we know which brand we are, find out what we're allowed. 2093 allowed = BRAND_OPTIONS[obj.brand].allowed_properties; 2094 2095 // replace obj.networks with array of nics. 2096 obj.nics = []; 2097 for (nic in obj.networks) { 2098 obj.nics.push(obj.networks[nic]); 2099 } 2100 delete obj.networks; 2101 2102 // replace obj.devices with array of disks. 2103 if (allowed.hasOwnProperty('disks')) { 2104 obj.disks = []; 2105 for (disk in obj.devices) { 2106 obj.disks.push(obj.devices[disk]); 2107 } 2108 } 2109 delete obj.devices; 2110 2111 if (!BRAND_OPTIONS.hasOwnProperty(obj.brand)) { 2112 throw new Error('unable to handle brand ' + obj.brand); 2113 } 2114 2115 if (BRAND_OPTIONS[obj.brand].features.use_vm_autoboot) { 2116 obj.autoboot = obj.vm_autoboot; 2117 delete obj.vm_autoboot; 2118 } 2119 2120 // apply the XML_PROPERTY_TRANSFORMs 2121 applyTransforms(obj); 2122 2123 // probe for some fields on disks if this brand of zone supports them. 2124 if (allowed.hasOwnProperty('disks') 2125 && (allowed.disks.indexOf('create') !== -1)) { 2126 2127 for (disk in obj.disks) { 2128 disk = obj.disks[disk]; 2129 2130 if (preload_data.hasOwnProperty('dsinfo')) { 2131 dsinfo = preload_data.dsinfo; 2132 if (dsinfo.hasOwnProperty('mountpoints') 2133 && dsinfo.mountpoints.hasOwnProperty(disk.path)) { 2134 2135 disk.zfs_filesystem = dsinfo.mountpoints[disk.path]; 2136 disk.zpool = disk.zfs_filesystem.split('/')[0]; 2137 } else { 2138 log.trace('no mountpoint data for ' + disk.path); 2139 } 2140 } 2141 } 2142 } 2143 2144 if (obj.hasOwnProperty('transition')) { 2145 fields = rtrim(obj.transition).split(':'); 2146 if (fields.length === 3) { 2147 delete obj.transition; 2148 obj.state = fields[0]; 2149 obj.transition_to = fields[1]; 2150 obj.transition_expire = fields[2]; 2151 } else { 2152 log.debug('getVmobj() ignoring bad value for ' 2153 + 'transition "' + obj.transition + '"'); 2154 } 2155 } 2156 2157 // sort the disks + nics by index 2158 if (obj.hasOwnProperty('disks')) { 2159 indexSort(obj.disks, 'path', /^.*-disk(\d+)$/); 2160 } 2161 if (obj.hasOwnProperty('nics')) { 2162 indexSort(obj.nics, 'interface', /^net(\d+)$/); 2163 } 2164 if (obj.hasOwnProperty('filesystems')) { 2165 indexSort(obj.filesystems, 'target', /^(.*)$/); 2166 } 2167 2168 callback(null, obj); 2169 }); 2170 } 2171 2172 function setQuota(dataset, quota, log, callback) 2173 { 2174 var newval; 2175 2176 assert(log, 'no logger passed to setQuota()'); 2177 2178 if (!dataset) { 2179 callback(new Error('Invalid dataset: "' + dataset + '"')); 2180 return; 2181 } 2182 2183 if (quota === 0 || quota === '0') { 2184 newval = 'none'; 2185 } else { 2186 newval = quota.toString() + 'g'; 2187 } 2188 2189 zfs(['set', 'quota=' + newval, dataset], log, function (err, fds) { 2190 if (err) { 2191 log.error('setQuota() cmd failed: ' + fds.stderr); 2192 callback(new Error(rtrim(fds.stderr))); 2193 } else { 2194 callback(); 2195 } 2196 }); 2197 } 2198 2199 function cleanDatasetObject(obj) 2200 { 2201 var number_fields = [ 2202 'avail', 2203 'available', 2204 'copies', 2205 'creation', 2206 'filesystem_limit', 2207 'quota', 2208 'recsize', 2209 'recordsize', 2210 'refer', 2211 'referenced', 2212 'refquota', 2213 'refreserv', 2214 'refreservation', 2215 'reserv', 2216 'reservation', 2217 'snapshot_limit', 2218 'usedbychildren', 2219 'usedbydataset', 2220 'usedbyrefreservation', 2221 'usedbysnapshots', 2222 'used', 2223 'userrefs', 2224 'utf8only', 2225 'version', 2226 'volblock', 2227 'volblocksize', 2228 'volsize', 2229 'written' 2230 ]; 2231 2232 // We should always have mountpoint, dataset and type because we force them 2233 // to be included in zfsList() 2234 assert(obj.hasOwnProperty('mountpoint'), 'cleanDatasetObject(' 2235 + JSON.stringify(obj) + '): missing mountpoint'); 2236 assert(obj.hasOwnProperty('name'), 'cleanDatasetObject(' 2237 + JSON.stringify(obj) + '): missing name'); 2238 assert(obj.hasOwnProperty('type'), 'cleanDatasetObject(' 2239 + JSON.stringify(obj) + '): missing type'); 2240 2241 // convert numeric fields to proper numbers 2242 number_fields.forEach(function (field) { 2243 if (obj.hasOwnProperty(field) && obj[field] !== '-') { 2244 obj[field] = Number(obj[field]); 2245 } 2246 }); 2247 2248 if (obj.type === 'volume') { 2249 obj.mountpoint = '/dev/zvol/rdsk/' + obj.name; 2250 } else if (obj.mountpoint === '-' || obj.mountpoint === 'legacy') { 2251 obj.mountpoint = '/' + obj.name; 2252 } 2253 } 2254 2255 function addDatasetResult(fields, types, results, line, log) 2256 { 2257 var dataset; 2258 var field; 2259 var lfields; 2260 var obj; 2261 var snapparts; 2262 var snapobj; 2263 2264 line = trim(line); 2265 2266 if (line.length === 0) { 2267 return; 2268 } 2269 2270 lfields = line.split(/\s+/); 2271 2272 if (lfields.length !== fields.length) { 2273 return; 2274 } 2275 2276 obj = {}; 2277 2278 for (field in fields) { 2279 obj[fields[field]] = lfields[field]; 2280 } 2281 2282 cleanDatasetObject(obj); 2283 2284 if (!results.hasOwnProperty('datasets')) { 2285 results.datasets = {}; 2286 } 2287 if (!results.hasOwnProperty('mountpoints')) { 2288 results.mountpoints = {}; 2289 } 2290 if (types.indexOf('snapshot') !== -1 && obj.type === 'snapshot') { 2291 if (!results.hasOwnProperty('snapshots')) { 2292 results.snapshots = {}; 2293 } 2294 2295 /* 2296 * For snapshots we store the snapname and optionally creation keyed by 2297 * dataset name So that we can include the list of snapshots for a 2298 * dataset on a VM. 2299 */ 2300 snapparts = obj.name.split('@'); 2301 assert.equal(snapparts.length, 2); 2302 dataset = snapparts[0]; 2303 snapobj = {snapname: snapparts[1], dataset: dataset}; 2304 if (!results.snapshots.hasOwnProperty(dataset)) { 2305 results.snapshots[dataset] = []; 2306 } 2307 if (obj.hasOwnProperty('creation')) { 2308 snapobj.created_at = obj.creation; 2309 } 2310 results.snapshots[dataset].push(snapobj); 2311 } 2312 2313 results.datasets[obj.name] = obj; 2314 2315 /* 2316 * snapshots don't have mountpoint that we care about and we don't count 2317 * 'none' as a mountpoint. If we otherwise have a mountpoint that looks like 2318 * a path, we add a pointer from that to the dataset name. 2319 */ 2320 if (obj.type !== 'snapshot' && obj.mountpoint[0] === '/') { 2321 /* 2322 * For zoned filesystems (delegated datasets) we don't use mountpoint as 2323 * this can be changed from within the zone and is therefore not 2324 * reliable. Also, when a delegated dataset is assigned but the zone's 2325 * not been booted, the delegated dataset will not have the 'zoned' 2326 * property. So we also check if the name ends in /data. 2327 */ 2328 if (obj.hasOwnProperty('zoned') && obj.zoned === 'on') { 2329 // don't add zoned datasets to mountpoints 2330 /*jsl:pass*/ 2331 } else if (obj.name.split('/')[2] === 'data') { 2332 // name is /data, skip 2333 /*jsl:pass*/ 2334 } else { 2335 // here we have what looks like a normal non-zoned dataset that's 2336 // probably a zoneroot, add to mountpoints mapping. 2337 results.mountpoints[obj.mountpoint] = obj.name; 2338 } 2339 } 2340 } 2341 2342 /* 2343 * Arguments: 2344 * 2345 * 'fields' - should be an array of fields as listed in the zfs(1m) man page. 2346 * 'types' - should be one or more of: filesystem, snapshot, volume. 2347 * 'log' - should be a bunyan logger object. 2348 * 'callback' - will be called with (err, results) 2349 * 2350 * On failure: callback's err will be an Error object, ignore results. 2351 * On success: callback's results is an object with one or more members of: 2352 * 2353 * results.datasets 2354 * 2355 * keyed by dataset name containing the values for the requested fields. 2356 * 2357 * Eg: results.datasets['zones/cores'] === { name: 'zones/cores', ... } 2358 * 2359 * results.mountpoints 2360 * 2361 * keyed by mountpoint with value being dataset name. 2362 * 2363 * Eg: results.mountpoints['/zones/cores'] === 'zones/cores' 2364 * 2365 * results.snapshots 2366 * 2367 * keyed by dataset with value being array of snapname and created_at. 2368 * 2369 * Eg: results.snapshots['/zones/cores'] === ['snap1', ...] 2370 * 2371 * For non-zoned filesystem datasets (these should be the zoneroot datasets), 2372 * you can use mountpoint which comes from zoneadm's "cheap" info and use that 2373 * to get to the dataset and from datasets[dataset] get the info. 2374 * 2375 * For volumes (KVM VM's disks) you can also use mountpoint as we'll set that 2376 * to the block device path and that's available from the devices section of 2377 * the zoneconfig. 2378 * 2379 * For zoned filesystems (delegated datasets) use the dataset name, as the 2380 * mountpoint can be changed from within the zone. 2381 * 2382 */ 2383 function zfsList(fields, types, log, callback) { 2384 var args; 2385 var buffer = ''; 2386 var lines; 2387 var cmd = '/usr/sbin/zfs'; 2388 var req_fields = ['mountpoint', 'name', 'type']; 2389 var results = {}; 2390 var zfs_child; 2391 2392 assert(Array.isArray(types)); 2393 assert(Array.isArray(fields)); 2394 assert(log, 'no logger passed to zfsList()'); 2395 2396 // add any missing required fields 2397 req_fields.forEach(function (field) { 2398 if (fields.indexOf(field) === -1) { 2399 fields.push(field); 2400 } 2401 }); 2402 2403 args = ['list', '-H', '-p', '-t', types.join(','), '-o', fields.join(',')]; 2404 2405 log.debug(cmd + ' ' + args.join(' ')); 2406 2407 zfs_child = spawn(cmd, args, {'customFds': [-1, -1, -1]}); 2408 log.debug('zfs running with pid ' + zfs_child.pid); 2409 2410 zfs_child.stdout.on('data', function (data) { 2411 var line; 2412 2413 buffer += data.toString(); 2414 lines = buffer.split('\n'); 2415 while (lines.length > 1) { 2416 line = lines.shift(); 2417 2418 // Add this line to results 2419 addDatasetResult(fields, types, results, line, log); 2420 } 2421 buffer = lines.pop(); 2422 }); 2423 2424 // doesn't take input. 2425 zfs_child.stdin.end(); 2426 2427 zfs_child.on('exit', function (code) { 2428 log.debug('zfs process ' + zfs_child.pid + ' exited with code: ' 2429 + code); 2430 if (code === 0) { 2431 callback(null, results); 2432 } else { 2433 callback(new Error('zfs exited prematurely with code: ' + code)); 2434 } 2435 }); 2436 } 2437 2438 /* 2439 * This queue is used to handle zfs list requests. We do this because of OS-1834 2440 * in order to only run one 'zfs list' at a time. If we need to get data from 2441 * 'zfs list', the parameters we want to list are pushed onto this queue. If a 2442 * list is already running with the same parameters, we'll return the output 2443 * from that one when it completes to all the consumers. If there's not one 2444 * running, or the parameters are different, this set of parameters will be 2445 * pushed onto the tail of the queue. The queue is processed serially so long 2446 * as there are active requests. 2447 */ 2448 zfs_list_queue = async.queue(function (task, callback) { 2449 2450 var fields = task.fields; 2451 var log = task.log; 2452 var started = Date.now(0); 2453 var types = task.types; 2454 2455 zfsList(fields, types, log, function (err, data) { 2456 var emitter = zfs_list_in_progress[task]; 2457 2458 delete zfs_list_in_progress[task]; 2459 emitter.emit('result', err, data); 2460 emitter.removeAllListeners('result'); 2461 2462 log.debug('zfs list took ' + (Date.now(0) - started) + ' ms'); 2463 callback(); 2464 }); 2465 2466 }, 1); 2467 2468 zfs_list_queue.drain = function () { 2469 // We use the global log here because this queue is not tied to one action. 2470 VM.log.trace('zfs_list_queue is empty'); 2471 }; 2472 2473 function getZfsList(fields, types, log, callback) { 2474 var sorted_fields; 2475 var sorted_types; 2476 var task; 2477 2478 sorted_fields = fields.slice().sort(); 2479 sorted_types = types.slice().sort(); 2480 2481 task = {types: sorted_types, fields: sorted_fields, log: log}; 2482 2483 try { 2484 zfs_list_in_progress[task].on('result', callback); 2485 } catch (e) { 2486 if ((e instanceof TypeError) 2487 && (!zfs_list_in_progress.hasOwnProperty(task) 2488 || !zfs_list_in_progress[task].hasOwnProperty('on'))) { 2489 2490 zfs_list_in_progress[task] = new EventEmitter(); 2491 zfs_list_in_progress[task].on('result', callback); 2492 zfs_list_in_progress[task].setMaxListeners(0); 2493 zfs_list_queue.push(task); 2494 2495 // callback() will get called when 'result' is emitted. 2496 } else { 2497 callback(e); 2498 } 2499 } 2500 } 2501 2502 function loadDatasetInfo(fields, log, callback) 2503 { 2504 var zfs_fields = []; 2505 var zfs_types = []; 2506 2507 assert(log, 'no logger passed to loadDataset()'); 2508 2509 function addField(name) { 2510 if (zfs_fields.indexOf(name) === -1) { 2511 zfs_fields.push(name); 2512 } 2513 } 2514 2515 function addType(name) { 2516 if (zfs_types.indexOf(name) === -1) { 2517 zfs_types.push(name); 2518 } 2519 } 2520 2521 if (!fields || fields.length < 1) { 2522 // Default to grabbing everything we might possibly need. 2523 zfs_fields = ['name', 'quota', 'volsize', 'mountpoint', 'type', 2524 'compression', 'recsize', 'volblocksize', 'zoned', 'creation', 2525 'refreservation']; 2526 zfs_types = ['filesystem', 'snapshot', 'volume']; 2527 } else { 2528 if (fields.indexOf('disks') !== -1) { 2529 addField('compression'); 2530 addField('volsize'); 2531 addField('volblocksize'); 2532 addField('refreservation'); 2533 addType('volume'); 2534 } 2535 if (fields.indexOf('snapshots') !== -1) { 2536 addField('creation'); 2537 addType('snapshot'); 2538 addType('filesystem'); 2539 addType('volume'); 2540 } 2541 if (fields.indexOf('create_timestamp') !== -1) { 2542 // We might fall back to creation on the dataset for 2543 // create_timestamp if we have no create-timestamp attr. 2544 addField('creation'); 2545 addType('filesystem'); 2546 } 2547 if ((fields.indexOf('zfs_root_compression') !== -1) 2548 || (fields.indexOf('zfs_data_compression') !== -1)) { 2549 2550 addField('compression'); 2551 addType('filesystem'); 2552 } 2553 if ((fields.indexOf('zfs_root_recsize') !== -1) 2554 || (fields.indexOf('zfs_data_recsize') !== -1)) { 2555 2556 addField('recsize'); 2557 addType('filesystem'); 2558 } 2559 if (fields.indexOf('quota') !== -1) { 2560 addField('quota'); 2561 addType('filesystem'); 2562 } 2563 // both zpool and zfs_filesystem come from 'name' 2564 if (fields.indexOf('zpool') !== -1 2565 || fields.indexOf('zfs_filesystem') !== -1) { 2566 2567 addField('name'); 2568 addType('filesystem'); 2569 } 2570 if (zfs_fields.length > 0) { 2571 // we have some fields so we need to zfs, make sure we have name, 2572 // mountpoint and type which we always need if we get anything. 2573 addField('name'); 2574 addField('mountpoint'); 2575 addField('type'); 2576 2577 if (zfs_types.indexOf('filesystem') !== -1) { 2578 // to differentiate between delegated and root filesystems 2579 addField('zoned'); 2580 } 2581 } else { 2582 log.debug('no need to call zfs'); 2583 callback(null, { 2584 datasets: {}, 2585 mountpoints: {}, 2586 snapshots: {} 2587 }); 2588 return; 2589 } 2590 } 2591 2592 /* 2593 * NOTE: 2594 * in the future, the plan is to have the list of types and fields 2595 * be dynamic based what's actually needed to handle the request. 2596 */ 2597 2598 getZfsList(zfs_fields, zfs_types, log, callback); 2599 } 2600 2601 function loadJsonConfig(vmobj, cfg, log, callback) 2602 { 2603 var filename; 2604 2605 assert(log, 'no logger passed to loadJsonConfig()'); 2606 2607 if (vmobj.zonepath) { 2608 filename = vmobj.zonepath + '/config/' + cfg + '.json'; 2609 log.trace('loadJsonConfig() loading ' + filename); 2610 2611 fs.readFile(filename, function (error, data) { 2612 var json = {}; 2613 2614 if (error) { 2615 if (error.code === 'ENOENT') { 2616 log.debug('Skipping nonexistent file ' + filename); 2617 } else { 2618 log.error(error, 2619 'loadJsonConfig() failed to load ' + filename); 2620 callback(error, {}); 2621 return; 2622 } 2623 } else { 2624 try { 2625 json = JSON.parse(data.toString()); 2626 } catch (e) { 2627 json = {}; 2628 } 2629 } 2630 2631 callback(null, json); 2632 }); 2633 } else { 2634 callback(null, {}); 2635 } 2636 } 2637 2638 /* 2639 * This preloads some data for us that comes from commands which output for 2640 * *all* VMs. This allows us to just run these (expensive) commands once 2641 * instead of having to run them for each VM. 2642 * 2643 */ 2644 function preloadZoneData(uuid, options, callback) 2645 { 2646 var data = {}; 2647 var log; 2648 2649 assert(options.log, 'no logger passed to preloadZoneData()'); 2650 log = options.log; 2651 2652 // NOTE: uuid can be null, in which case we get data for all VMs. 2653 2654 async.series([ 2655 function (cb) { 2656 // We always do this (calls `zoneadm list -vc`) since we always 2657 // need to know which zones exist. 2658 getZoneRecords(uuid, log, function (err, records) { 2659 if (!err) { 2660 data.records = records; 2661 } 2662 cb(err); 2663 }); 2664 }, function (cb) { 2665 var fields; 2666 2667 if (options.hasOwnProperty('fields')) { 2668 fields = options.fields; 2669 } else { 2670 fields = []; 2671 } 2672 2673 loadDatasetInfo(fields, log, function (err, dsinfo) { 2674 if (!err) { 2675 data.dsinfo = dsinfo; 2676 } 2677 cb(err); 2678 }); 2679 }, function (cb) { 2680 if (options.hasOwnProperty('fields') 2681 && (options.fields.indexOf('server_uuid') === -1 2682 && options.fields.indexOf('datacenter_name') === -1 2683 && options.fields.indexOf('headnode_id') === -1)) { 2684 2685 // we don't need any fields that come from sysinfo. 2686 log.debug('no need to call sysinfo, no sysinfo fields in list'); 2687 data.sysinfo = {}; 2688 cb(); 2689 return; 2690 } 2691 2692 VM.getSysinfo([], {log: log}, function (err, sysinfo) { 2693 if (!err) { 2694 data.sysinfo = sysinfo; 2695 } 2696 cb(err); 2697 }); 2698 }, function (cb) { 2699 var u; 2700 var uuids = []; 2701 2702 if (options.hasOwnProperty('fields') 2703 && options.fields.indexOf('pid') === -1) { 2704 2705 log.debug('no need to check PID files, PID not in field list'); 2706 cb(); 2707 return; 2708 } 2709 2710 // get the PID values from running KVM VMs 2711 2712 for (u in data.records) { 2713 uuids.push(u); 2714 } 2715 async.forEachSeries(uuids, function (z_uuid, zcb) { 2716 var filename; 2717 var z = data.records[z_uuid]; 2718 2719 // NOTE: z.state here is equivalent to zone_state not state. 2720 if (z && BRAND_OPTIONS[z.brand].hasOwnProperty('features') 2721 && BRAND_OPTIONS[z.brand].features.pid_file 2722 && z.state === 'running') { 2723 2724 // ensure pid_file is safe 2725 try { 2726 assertSafeZonePath(path.join(z.zonepath, '/root'), 2727 BRAND_OPTIONS[z.brand].features.pid_file, 2728 {type: 'file', enoent_ok: true}); 2729 } catch (e) { 2730 // We log an error here, but not being able to get 2731 // the PID for one broken machine should not impact the 2732 // ability to get a list of all machines, so we just 2733 // skip adding a PID and log an error here. 2734 log.error(e, 'Unsafe path for ' + z.uuid + ' cannot ' 2735 + 'check for PID file: ' + e.message); 2736 zcb(); 2737 return; 2738 } 2739 2740 filename = path.join(z.zonepath, 'root', 2741 BRAND_OPTIONS[z.brand].features.pid_file); 2742 log.debug('checking for ' + filename); 2743 2744 fs.readFile(filename, 2745 function (error, filedata) { 2746 2747 var pid; 2748 2749 if (!error) { 2750 pid = Number(trim(filedata.toString())); 2751 if (pid > 0) { 2752 z.pid = pid; 2753 log.debug('found PID ' + pid + ' for ' 2754 + z.uuid); 2755 } 2756 } 2757 if (error && error.code === 'ENOENT') { 2758 // don't return error in this case because it just 2759 // didn't exist 2760 log.debug('no PID file for ' + z.uuid); 2761 zcb(); 2762 } else { 2763 zcb(error); 2764 } 2765 }); 2766 } else { 2767 zcb(); 2768 } 2769 }, function (err) { 2770 cb(err); 2771 }); 2772 } 2773 ], function (err, res) { 2774 log.trace('leaving preloadZoneData()'); 2775 callback(err, data); 2776 }); 2777 } 2778 2779 function getZoneRecords(uuid, log, callback) 2780 { 2781 var args = []; 2782 var buffer = ''; 2783 var cmd = '/usr/sbin/zoneadm'; 2784 var line_count = 0; 2785 var lines; 2786 var results = {}; 2787 var zadm; 2788 var zadm_stderr = ''; 2789 2790 assert(log, 'no logger passed to getZoneRecords()'); 2791 2792 if (uuid) { 2793 // this gives us zone info if uuid is *either* a zonename or uuid 2794 if (isUUID(uuid)) { 2795 args.push('-z'); 2796 args.push(uuid); 2797 args.push('-u'); 2798 args.push(uuid); 2799 } else { 2800 args.push('-z'); 2801 args.push(uuid); 2802 } 2803 } 2804 args.push('list'); 2805 args.push('-p'); 2806 if (!uuid) { 2807 args.push('-c'); 2808 } 2809 2810 log.debug(cmd + ' ' + args.join(' ')); 2811 2812 zadm = spawn(cmd, args, {'customFds': [-1, -1, -1]}); 2813 log.debug('zoneadm running with PID ' + zadm.pid); 2814 2815 zadm.stderr.on('data', function (data) { 2816 zadm_stderr += data.toString(); 2817 }); 2818 2819 zadm.stdout.on('data', function (data) { 2820 var fields; 2821 var line; 2822 var obj; 2823 2824 buffer += data.toString(); 2825 lines = buffer.split('\n'); 2826 while (lines.length > 1) { 2827 line = lines.shift(); 2828 line_count++; 2829 fields = rtrim(line).split(':'); 2830 if (fields.length === 8 && fields[1] !== 'global') { 2831 obj = { 2832 'zoneid': Number(fields[0]), 2833 'zonename': fields[1], 2834 'state': fields[2], 2835 'zonepath': fields[3], 2836 'uuid': fields[4], 2837 'brand': fields[5], 2838 'ip_type': fields[6] 2839 }; 2840 log.trace('loaded: ' + JSON.stringify(obj)); 2841 // XXX zones in some states have no uuid. We should either fix 2842 // that or use zonename for those. 2843 results[obj.uuid] = obj; 2844 } else if (line.replace(/ /g, '').length > 0) { 2845 log.debug('getZoneRecords(' + uuid + ') ignoring: ' + line); 2846 } 2847 } 2848 buffer = lines.pop(); 2849 }); 2850 2851 // doesn't take input. 2852 zadm.stdin.end(); 2853 2854 zadm.on('close', function (code) { 2855 var errmsg; 2856 var new_err; 2857 2858 log.debug('zoneadm process ' + zadm.pid + ' exited with code: ' 2859 + code + ' (' + line_count + ' lines to stdout)'); 2860 if (code === 0) { 2861 callback(null, results); 2862 } else { 2863 errmsg = rtrim(zadm_stderr); 2864 new_err = new Error(errmsg); 2865 if (errmsg.match(/No such zone configured$/)) { 2866 // not existing isn't always a problem (eg. existence check) 2867 new_err.code = 'ENOENT'; 2868 } else { 2869 log.error({err: new_err, stderr: zadm_stderr}, 2870 'getZoneRecords() zoneadm "' + args.join(',') + '" failed'); 2871 } 2872 callback(new_err); 2873 return; 2874 } 2875 }); 2876 } 2877 2878 exports.flatten = function (vmobj, key) 2879 { 2880 var index; 2881 var tokens = key.split('.'); 2882 2883 // NOTE: VM.flatten() currently doesn't produce any logs 2884 2885 if (tokens.length === 3 2886 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(tokens[0]) !== -1) { 2887 2888 if (!vmobj.hasOwnProperty(tokens[0])) { 2889 return undefined; 2890 } 2891 if (!vmobj[tokens[0]].hasOwnProperty(tokens[1])) { 2892 return undefined; 2893 } 2894 return vmobj[tokens[0]][tokens[1]][tokens[2]]; 2895 } 2896 2897 if (tokens.length === 2 2898 && VM.FLATTENABLE_HASH_KEYS.indexOf(tokens[0]) !== -1) { 2899 2900 if (!vmobj.hasOwnProperty(tokens[0])) { 2901 return undefined; 2902 } 2903 return vmobj[tokens[0]][tokens[1]]; 2904 } 2905 2906 if (tokens.length === 2 2907 && VM.FLATTENABLE_ARRAYS.indexOf(tokens[0]) !== -1) { 2908 2909 index = Number(tokens[1]); 2910 2911 if (!vmobj.hasOwnProperty(tokens[0])) { 2912 return undefined; 2913 } 2914 2915 if (index === NaN || index < 0 2916 || !vmobj[tokens[0]].hasOwnProperty(index)) { 2917 2918 return undefined; 2919 } 2920 return vmobj[tokens[0]][index]; 2921 } 2922 2923 return vmobj[key]; 2924 }; 2925 2926 function getLastModified(vmobj, log) 2927 { 2928 var files = []; 2929 var file; 2930 var stat; 2931 var timestamp = 0; 2932 2933 assert(log, 'no logger passed to getLastModified()'); 2934 2935 if (vmobj.zonepath) { 2936 files.push(path.join(vmobj.zonepath, '/config/metadata.json')); 2937 files.push(path.join(vmobj.zonepath, '/config/routes.json')); 2938 files.push(path.join(vmobj.zonepath, '/config/tags.json')); 2939 } else { 2940 log.debug('getLastModified() no zonepath!'); 2941 } 2942 2943 if (vmobj.hasOwnProperty('zonename')) { 2944 files.push('/etc/zones/' + vmobj.zonename + '.xml'); 2945 } else { 2946 log.debug('getLastModified() no zonename!'); 2947 } 2948 2949 for (file in files) { 2950 file = files[file]; 2951 try { 2952 stat = fs.statSync(file); 2953 if (stat.isFile()) { 2954 if ((timestamp === 0) || (Date.parse(stat.mtime) > timestamp)) { 2955 timestamp = Date.parse(stat.mtime); 2956 } 2957 } 2958 } catch (e) { 2959 if (e.code !== 'ENOENT') { 2960 log.error(e, 'Unable to get timestamp for "' + file + '":' 2961 + e.message); 2962 } 2963 } 2964 } 2965 2966 return ((new Date(timestamp)).toISOString()); 2967 } 2968 2969 function loadVM(uuid, data, options, callback) 2970 { 2971 var e; 2972 var info; 2973 var log; 2974 2975 assert(options.log, 'no logger passed to loadVM()'); 2976 log = options.log; 2977 2978 // XXX need to always have data when we get here 2979 info = data.records[uuid]; 2980 2981 if (!info) { 2982 e = new Error('VM.load() empty info when getting record ' 2983 + 'for vm ' + uuid); 2984 log.error(e); 2985 callback(e); 2986 return; 2987 } 2988 2989 getVmobj(info.zonename, data, options, function (err, vmobj) { 2990 if (err) { 2991 callback(err); 2992 return; 2993 } 2994 2995 function wantField(field) { 2996 if (options.hasOwnProperty('fields') 2997 && options.fields.indexOf(field) === -1) { 2998 2999 return false; 3000 } 3001 3002 return true; 3003 } 3004 3005 // We got some bits from `zoneadm list` as <info> here, and since we 3006 // already got that data, adding it to the object here is cheap. We also 3007 // need some of these properties to be able to get others later, so we 3008 // add them all now. If they're unwanted they'll be removed from the 3009 // final object. 3010 vmobj.uuid = info.uuid; 3011 vmobj.zone_state = info.state; 3012 3013 // In the case of 'configured' zones, we might only have zonename 3014 // because uuid isn't set yet. Because of that case, we set uuid 3015 // to zonename if it is in UUID form. 3016 if ((!vmobj.uuid || vmobj.uuid.length === 0) 3017 && isUUID(vmobj.zonename)) { 3018 3019 vmobj.uuid = vmobj.zonename; 3020 } 3021 3022 // These ones we never need elsewhere, so don't bother adding if we 3023 // don't need to. 3024 if (wantField('zoneid') && info.zoneid !== '-') { 3025 vmobj.zoneid = info.zoneid; 3026 } 3027 3028 if (wantField('pid') && info.pid) { 3029 vmobj.pid = info.pid; 3030 } 3031 3032 // find when we last modified this VM 3033 if (wantField('last_modified')) { 3034 vmobj.last_modified = getLastModified(vmobj, log); 3035 } 3036 3037 // If we want resolvers, (eg. OS-2194) we always add the array here 3038 // so you can tell that the resolvers are explicitly not set. 3039 if (wantField('resolvers') && !vmobj.hasOwnProperty('resolvers')) { 3040 vmobj.resolvers = []; 3041 } 3042 3043 // sysinfo has server_uuid and potentially some DC info 3044 if (data.hasOwnProperty('sysinfo')) { 3045 if (wantField('server_uuid') 3046 && data.sysinfo.hasOwnProperty('UUID')) { 3047 3048 vmobj.server_uuid = data.sysinfo.UUID; 3049 } 3050 if (wantField('datacenter_name') 3051 && data.sysinfo.hasOwnProperty('Datacenter Name')) { 3052 3053 vmobj.datacenter_name = data.sysinfo['Datacenter Name']; 3054 } 3055 if (wantField('headnode_id') 3056 && data.sysinfo.hasOwnProperty('Headnode ID')) { 3057 3058 vmobj.headnode_id = data.sysinfo['Headnode ID']; 3059 } 3060 } 3061 3062 // state could already be set here if it was overriden by a transition 3063 // that's in progress. So we only change if that's not the case. 3064 if (wantField('state')) { 3065 if (!vmobj.hasOwnProperty('state')) { 3066 if (info.state === 'installed') { 3067 vmobj.state = 'stopped'; 3068 } else { 3069 vmobj.state = info.state; 3070 } 3071 } 3072 3073 // If the zone has the 'failed' property it doesn't matter what 3074 // other state it might be in, we list its state as 'failed'. 3075 if (vmobj.failed) { 3076 vmobj.state = 'failed'; 3077 } 3078 } 3079 3080 async.series([ 3081 function (cb) { 3082 if (!wantField('customer_metadata') 3083 && !wantField('internal_metadata')) { 3084 3085 cb(); 3086 return; 3087 } 3088 3089 loadJsonConfig(vmobj, 'metadata', log, 3090 function (error, metadata) { 3091 if (error) { 3092 // when zone_state is 'incomplete' we could be 3093 // deleting it in which case metadata may already 3094 // be gone, ignore failure to load mdata when 3095 // 'incomplete' because of this. 3096 if (vmobj.zone_state === 'incomplete') { 3097 log.debug(error, 'zone is in state incomplete ' 3098 + 'ignoring error: ' + error.message); 3099 } else { 3100 cb(error); 3101 return; 3102 } 3103 } 3104 3105 if (wantField('customer_metadata')) { 3106 if (metadata.hasOwnProperty('customer_metadata')) { 3107 vmobj.customer_metadata 3108 = metadata.customer_metadata; 3109 } else { 3110 vmobj.customer_metadata = {}; 3111 } 3112 } 3113 3114 if (wantField('internal_metadata')) { 3115 if (metadata.hasOwnProperty('internal_metadata')) { 3116 vmobj.internal_metadata 3117 = metadata.internal_metadata; 3118 } else { 3119 vmobj.internal_metadata = {}; 3120 } 3121 } 3122 3123 cb(); 3124 }); 3125 }, function (cb) { 3126 if (!wantField('tags')) { 3127 cb(); 3128 return; 3129 } 3130 3131 loadJsonConfig(vmobj, 'tags', log, function (error, tags) { 3132 if (error) { 3133 // when zone_state is 'incomplete' we could be deleting 3134 // it in which case metadata may already be gone, ignore 3135 // failure to load mdata when 'incomplete' because of 3136 // this. 3137 if (vmobj.zone_state === 'incomplete') { 3138 log.debug(error, 'zone is in state incomplete ' 3139 + 'ignoring error: ' + error.message); 3140 } else { 3141 cb(error); 3142 return; 3143 } 3144 } 3145 vmobj.tags = tags; 3146 cb(); 3147 }); 3148 }, function (cb) { 3149 if (!wantField('routes')) { 3150 cb(); 3151 return; 3152 } 3153 3154 loadJsonConfig(vmobj, 'routes', log, function (error, routes) { 3155 if (error) { 3156 // same as tags above, if zone_state is 'incomplete' 3157 // we could be a file that's already gone 3158 if (vmobj.zone_state === 'incomplete') { 3159 log.debug(error, 'zone is in state incomplete ' 3160 + 'ignoring error: ' + error.message); 3161 } else { 3162 cb(error); 3163 return; 3164 } 3165 } 3166 vmobj.routes = routes; 3167 cb(); 3168 }); 3169 }, function (cb) { 3170 var dsinfo; 3171 var dsname; 3172 var dsobj; 3173 var d; 3174 var delegated; 3175 var disk; 3176 var ds; 3177 var filesys; 3178 var friendly_snap; 3179 var friendly_snapshots = []; 3180 var matches; 3181 var raw_snapshots = []; 3182 var snap; 3183 var snap_time; 3184 3185 // local alias, data.dsinfo should include all the info about 3186 // this VM's zoneroot that we care about here. 3187 dsinfo = data.dsinfo; 3188 3189 if (dsinfo.hasOwnProperty('mountpoints') 3190 && dsinfo.hasOwnProperty('datasets') 3191 && dsinfo.mountpoints.hasOwnProperty(vmobj.zonepath)) { 3192 3193 dsname = dsinfo.mountpoints[vmobj.zonepath]; 3194 dsobj = dsinfo.datasets[dsname]; 3195 3196 /* dsobj.quota is in bytes, we want GiB for vmobj.quota */ 3197 if (wantField('quota') && dsobj.hasOwnProperty('quota')) { 3198 vmobj.quota = (dsobj.quota / (1024 * 1024 * 1024)); 3199 log.trace('found quota "' + vmobj.quota + '" for ' 3200 + vmobj.uuid); 3201 } 3202 3203 if (wantField('create_timestamp') 3204 && !vmobj.hasOwnProperty('create_timestamp') 3205 && dsobj.hasOwnProperty('creation')) { 3206 3207 log.debug('VM has no create_timestamp, using creation ' 3208 + 'from ' + dsobj.name); 3209 vmobj.create_timestamp = 3210 (new Date(dsobj.creation * 1000)).toISOString(); 3211 } 3212 3213 if (wantField('zfs_root_compression') 3214 && dsobj.hasOwnProperty('compression') 3215 && (dsobj.compression !== 'off')) { 3216 3217 vmobj.zfs_root_compression = dsobj.compression; 3218 } 3219 3220 if (wantField('zfs_root_recsize') 3221 && dsobj.hasOwnProperty('recsize')) { 3222 3223 vmobj.zfs_root_recsize = dsobj.recsize; 3224 } 3225 3226 // Always add zfs_filesystem if we can because it's needed 3227 // to find other properties such as delegated_dataset. 3228 vmobj.zfs_filesystem = dsobj.name; 3229 3230 if (wantField('snapshots') 3231 && dsinfo.hasOwnProperty('snapshots') 3232 && dsinfo.snapshots 3233 .hasOwnProperty(vmobj.zfs_filesystem)) { 3234 3235 raw_snapshots = raw_snapshots.concat( 3236 dsinfo.snapshots[vmobj.zfs_filesystem]); 3237 } 3238 3239 log.trace('found dataset "' + vmobj.zfs_filesystem 3240 + '" for ' + vmobj.uuid); 3241 } else { 3242 log.trace('no dsinfo for ' + vmobj.uuid + ': ' 3243 + vmobj.zonepath); 3244 } 3245 3246 // delegated datasets are keyed on the dataset name instead of 3247 // mountpoint, since mountpoint can change in a zone. 3248 if (vmobj.hasOwnProperty('zfs_filesystem')) { 3249 delegated = vmobj.zfs_filesystem + '/data'; 3250 if (dsinfo.datasets.hasOwnProperty(delegated)) { 3251 dsobj = dsinfo.datasets[delegated]; 3252 3253 if (dsobj.hasOwnProperty('compression') 3254 && (dsobj.compression !== 'off')) { 3255 3256 vmobj.zfs_data_compression = dsobj.compression; 3257 } 3258 if (dsobj.hasOwnProperty('recsize')) { 3259 vmobj.zfs_data_recsize = dsobj.recsize; 3260 } 3261 3262 // If there are snapshots for this dataset, add them 3263 if (DISABLED) { 3264 // XXX currently only support snapshot on 3265 // zfs_filesystem 3266 if (dsinfo.hasOwnProperty('snapshots') 3267 && dsinfo.snapshots.hasOwnProperty(delegated)) { 3268 3269 raw_snapshots = raw_snapshots 3270 .concat(dsinfo.snapshots[delegated]); 3271 } 3272 } 3273 } else { 3274 log.trace('no dsinfo for delegated dataset: ' 3275 + delegated); 3276 } 3277 3278 vmobj.zpool = 3279 vmobj.zfs_filesystem.split('/')[0]; 3280 } 3281 3282 if (wantField('disks') && vmobj.hasOwnProperty('disks')) { 3283 for (d in vmobj.disks) { 3284 d = vmobj.disks[d]; 3285 if (d.hasOwnProperty('path') 3286 && dsinfo.mountpoints.hasOwnProperty(d.path)) { 3287 3288 dsname = dsinfo.mountpoints[d.path]; 3289 dsobj = dsinfo.datasets[dsname]; 3290 3291 if (dsobj.hasOwnProperty('volsize')) { 3292 3293 /* dsobj.volsize is in bytes, we want MiB */ 3294 d.size = (dsobj.volsize / (1024 * 1024)); 3295 log.debug('found size=' + d.size + ' for ' 3296 + JSON.stringify(d)); 3297 } 3298 if (dsobj.hasOwnProperty('compression')) { 3299 d.compression = dsobj.compression; 3300 } 3301 if (dsobj.hasOwnProperty('refreservation')) { 3302 /* dsobj.refreservation is in bytes, want MiB */ 3303 d.refreservation 3304 = (dsobj.refreservation / (1024 * 1024)); 3305 log.debug('found refreservation=' 3306 + d.refreservation + ' for ' 3307 + JSON.stringify(d)); 3308 } 3309 if (dsobj.hasOwnProperty('volblocksize')) { 3310 d.block_size = dsobj.volblocksize; 3311 } 3312 3313 // If there are snapshots for this dataset, add them 3314 // to the list. 3315 if (DISABLED) { 3316 // XXX currently only support snapshots on 3317 // zfs_filesystem 3318 if (dsinfo.hasOwnProperty('snapshots') 3319 && dsinfo.snapshots.hasOwnProperty( 3320 d.zfs_filesystem)) { 3321 3322 raw_snapshots = raw_snapshots.concat(dsinfo 3323 .snapshots[d.zfs_filesystem]); 3324 } 3325 } 3326 } else if (d.hasOwnProperty('path')) { 3327 d.missing = true; 3328 } else { 3329 log.warn('no dsinfo and no path for ' 3330 + JSON.stringify(d)); 3331 } 3332 } 3333 } 3334 3335 // snapshots here is the raw list of snapshots, now we need to 3336 // convert it to the "friendly" list of snapshots. 3337 if (wantField('snapshots')) { 3338 for (snap in raw_snapshots) { 3339 snap = raw_snapshots[snap]; 3340 3341 matches = snap.snapname.match(/^vmsnap-(.*)$/); 3342 if (matches && matches[1]) { 3343 friendly_snap = {name: matches[1]}; 3344 if (snap.hasOwnProperty('created_at')) { 3345 snap_time 3346 = new Date(snap.created_at * 1000); // in ms 3347 friendly_snap.created_at 3348 = snap_time.toISOString(); 3349 } 3350 friendly_snapshots.push(friendly_snap); 3351 } else { 3352 log.debug('ignoring unfriendly ' + snap.snapname); 3353 continue; 3354 } 3355 } 3356 // sort the snapshots with newest first. 3357 friendly_snapshots.sort(function (a, b) { 3358 if (a.created_at > b.created_at) { 3359 return -1; 3360 } 3361 if (a.created_at < b.created_at) { 3362 return 1; 3363 } 3364 return 0; // equal 3365 }); 3366 vmobj.snapshots = friendly_snapshots; 3367 } 3368 3369 if (vmobj.state === 'receiving') { 3370 vmobj.missing = { 'datasets': [], 'disks': [], 3371 'filesystems': [] }; 3372 if (!fs.existsSync(vmobj.zonepath)) { 3373 vmobj.missing.datasets.push(vmobj.zonepath.substr(1)); 3374 } 3375 for (ds in vmobj.datasets) { 3376 ds = vmobj.datasets[ds]; 3377 vmobj.missing.datasets.push(ds); 3378 } 3379 for (filesys in vmobj.filesystems) { 3380 filesys = vmobj.filesystems[filesys]; 3381 if (filesys.hasOwnProperty('source')) { 3382 vmobj.missing.filesystems.push(filesys.source); 3383 } 3384 } 3385 for (disk in vmobj.disks) { 3386 disk = vmobj.disks[disk]; 3387 if (disk.hasOwnProperty('missing')) { 3388 vmobj.missing.disks.push(disk.path); 3389 } 3390 } 3391 } 3392 3393 cb(); 3394 } 3395 ], function (error) { 3396 callback(error, vmobj); 3397 }); 3398 3399 }); 3400 } 3401 3402 exports.load = function (uuid, options, callback) 3403 { 3404 var log; 3405 var load_opts = {}; 3406 3407 // This is a wrapper so that other internal functions here (such as lookup) 3408 // can do smart things like check the quota for each VM with a separate call 3409 // to zfs get. 3410 3411 // options is optional 3412 if (arguments.length === 2) { 3413 callback = arguments[1]; 3414 options = {}; 3415 } 3416 3417 ensureLogging(false); 3418 if (options.hasOwnProperty('log')) { 3419 log = options.log; 3420 } else { 3421 log = VM.log.child({action: 'load', vm: uuid}); 3422 } 3423 3424 load_opts.log = log; 3425 if (options.hasOwnProperty('fields')) { 3426 load_opts.fields = options.fields; 3427 } 3428 3429 preloadZoneData(uuid, load_opts, function (error, data) { 3430 if (error) { 3431 if (options.missing_ok && error.code === 'ENOENT') { 3432 // we're expecting the zone to be gone in this case (eg. delete) 3433 log.debug('VM ' + uuid + ' does not exist (as expected)'); 3434 } else { 3435 log.error(error, 'VM.load() failed to get zone record' 3436 + ' for ' + uuid); 3437 } 3438 callback(error); 3439 } else { 3440 loadVM(uuid, data, load_opts, function (e, vmobj) { 3441 if (e) { 3442 callback(e); 3443 return; 3444 } 3445 3446 if (load_opts.hasOwnProperty('fields')) { 3447 // clean out unwanted fields 3448 Object.keys(vmobj).forEach(function (key) { 3449 if (options.fields.indexOf(key) === -1) { 3450 delete vmobj[key]; 3451 } 3452 }); 3453 } 3454 callback(null, vmobj); 3455 }); 3456 } 3457 }); 3458 }; 3459 3460 function fixMac(str) 3461 { 3462 var fixed = []; 3463 var octet; 3464 var octets = str.split(':'); 3465 3466 for (octet in octets) { 3467 if (octets.hasOwnProperty(octet)) { 3468 octet = parseInt(octets[octet], 16); 3469 if (octet === 'nan') { 3470 octet = 0; 3471 } 3472 fixed.push(sprintf('%02x', octet)); 3473 } 3474 } 3475 3476 return fixed.join(':'); 3477 } 3478 3479 // zonecfg requires removing leading 0's in MACs like 01:02:03:04:05:06 3480 // This function takes a MAC in normal form and puts it in the goofy form 3481 // zonecfg wants. 3482 function ruinMac(mac) 3483 { 3484 var part; 3485 var parts; 3486 var out = []; 3487 3488 parts = mac.split(':'); 3489 3490 for (part in parts) { 3491 part = ltrim(parts[part], '0'); 3492 if (part.length === 0) { 3493 part = '0'; 3494 } 3495 out.push(part); 3496 } 3497 3498 return (out.join(':')); 3499 } 3500 3501 function matcher(zone, search) 3502 { 3503 var fields; 3504 var found; 3505 var i; 3506 var key; 3507 var parameters_matched = 0; 3508 var regex; 3509 var target; 3510 3511 function find_match(k, targ) { 3512 var value = VM.flatten(zone, k); 3513 3514 if (!regex && k.match(/^nics\..*\.mac$/)) { 3515 // Fix for broken SmartOS MAC format 3516 targ = fixMac(targ); 3517 } 3518 3519 if (regex && (value !== undefined) && value.toString().match(targ)) { 3520 found = true; 3521 } else if ((value !== undefined) 3522 && value.toString() === targ.toString()) { 3523 found = true; 3524 } 3525 } 3526 3527 for (key in search) { 3528 found = false; 3529 regex = false; 3530 3531 target = search[key]; 3532 if (target[0] === '~') { 3533 regex = true; 3534 target = new RegExp(target.substr(1), 'i'); 3535 } 3536 3537 fields = key.split('.'); 3538 if (fields.length === 3 && fields[1] === '*' 3539 && zone.hasOwnProperty(fields[0]) 3540 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(fields[0]) !== -1) { 3541 3542 // Special case: for eg. nics.*.ip, we want to loop through all nics 3543 for (i = 0; i < zone[fields[0]].length; i++) { 3544 fields[1] = i; 3545 find_match(fields.join('.'), target); 3546 } 3547 } else { 3548 find_match(key, target); 3549 } 3550 3551 if (!found) { 3552 return false; 3553 } else { 3554 parameters_matched++; 3555 } 3556 } 3557 3558 if (parameters_matched > 0) { 3559 // we would have returned false from the loop had any parameters not 3560 // matched and we had at least one that did. 3561 return true; 3562 } 3563 3564 return false; 3565 } 3566 3567 exports.lookup = function (search, options, callback) 3568 { 3569 var log; 3570 var key; 3571 var matches; 3572 var need_fields = []; 3573 var preload_opts = {}; 3574 var quick_ok = true; 3575 var results = []; 3576 var transform; 3577 3578 // options is optional 3579 if (arguments.length === 2) { 3580 callback = arguments[1]; 3581 options = {}; 3582 } 3583 3584 ensureLogging(false); 3585 if (options.hasOwnProperty('log')) { 3586 log = options.log; 3587 } else { 3588 log = VM.log.child({action: 'lookup', search: search}); 3589 } 3590 3591 // XXX the 'transform' option is not intended to be public yet and should 3592 // only be used by tools willing to be rewritten if this is removed or 3593 // changed. 3594 if (options.hasOwnProperty('transform')) { 3595 transform = options.transform; 3596 } 3597 3598 // keep separate variable because we can have some fields we add below that 3599 // we need for searching, but shouldn't be in the output. 3600 if (options.hasOwnProperty('fields')) { 3601 need_fields = options.fields.slice(0); 3602 } 3603 3604 for (key in search) { 3605 // To be able to search on a field, that field needs to be added to 3606 // the objects, if user requested a set of fields missing the one 3607 // they're searching for, add it. 3608 matches = key.match(/^([^.]+)\./); 3609 if (matches) { 3610 if (need_fields.indexOf(matches[1]) == -1) { 3611 need_fields.push(matches[1]); 3612 } 3613 } else { 3614 if (need_fields.indexOf(key) == -1) { 3615 need_fields.push(key); 3616 } 3617 } 3618 } 3619 3620 // If all the keys we're searching for are in the QUICK_LOOKUP data, we 3621 // don't need the full zone records to locate the VMs we're interested in. 3622 for (key in need_fields) { 3623 if (QUICK_LOOKUP.indexOf(key) === -1) { 3624 quick_ok = false; 3625 } 3626 } 3627 3628 preload_opts.log = log; 3629 if (options.hasOwnProperty('fields')) { 3630 preload_opts.fields = need_fields; 3631 } 3632 3633 // This is used when you've specified fields to remove those that might 3634 // have been added as a group but are not wanted, or were added as 3635 // dependencies for looking up wanted fields, or for search. 3636 function filterFields(res) { 3637 res.forEach(function (result) { 3638 Object.keys(result).forEach(function (k) { 3639 if (options.fields.indexOf(k) === -1) { 3640 delete result[k]; 3641 } 3642 }); 3643 }); 3644 } 3645 3646 preloadZoneData(null, preload_opts, function (err, data) { 3647 var records = data.records; 3648 var uuids = []; 3649 3650 if (err) { 3651 callback(err); 3652 return; 3653 } 3654 3655 if (quick_ok) { 3656 var full_results = []; 3657 var load_opts = {}; 3658 var match; 3659 var regex; 3660 var source; 3661 var target; 3662 var u; 3663 var z; 3664 3665 if (err) { 3666 callback(err); 3667 return; 3668 } 3669 for (z in records) { 3670 z = records[z]; 3671 match = true; 3672 for (key in search) { 3673 regex = false; 3674 // force field type to string so that earlier transformed 3675 // number fields get back their match method and the 3676 // strict not-equal operator will work on number lookups 3677 source = '' + z[key]; 3678 target = search[key]; 3679 if (target[0] === '~') { 3680 target = new RegExp(target.substr(1), 'i'); 3681 regex = true; 3682 } 3683 if (regex && !source.match(target)) { 3684 match = false; 3685 } else if (!regex && (source !== search[key])) { 3686 match = false; 3687 } 3688 } 3689 if (match && z.uuid) { 3690 results.push(z.uuid); 3691 } 3692 } 3693 3694 load_opts.log = log; 3695 if (options.hasOwnProperty('fields') && need_fields.length > 0) { 3696 // we have a specific set of fields we want to grab 3697 load_opts.fields = need_fields; 3698 } else if (!options.full) { 3699 // we don't need all the data so what we already got is enough 3700 if (options.hasOwnProperty('fields')) { 3701 filterFields(results); 3702 } 3703 3704 callback(null, 3705 results.filter(function (res) { 3706 if (typeof (res) === 'object') { 3707 return (Object.keys(res).length > 0); 3708 } else { 3709 return (true); 3710 } 3711 }) 3712 ); 3713 return; 3714 } 3715 3716 function expander(uuid, cb) { 3717 loadVM(uuid, data, load_opts, function (e, obj) { 3718 if (e) { 3719 if (e.code === 'ENOENT') { 3720 // zone likely was deleted since lookup, ignore 3721 cb(); 3722 } else { 3723 cb(e); 3724 } 3725 } else { 3726 if (transform) { 3727 transform(obj); 3728 } 3729 full_results.push(obj); 3730 cb(); 3731 } 3732 }); 3733 } 3734 3735 async.forEachSeries(results, expander, function (e) { 3736 var res_list; 3737 3738 if (e) { 3739 log.error(e, 'VM.lookup failed to expand results: ' 3740 + e.message); 3741 callback(e); 3742 } else { 3743 res_list = full_results; 3744 if (options.hasOwnProperty('fields')) { 3745 filterFields(res_list); 3746 } 3747 callback(null, 3748 res_list.filter(function (res) { 3749 if (typeof (res) === 'object') { 3750 return (Object.keys(res).length > 0); 3751 } else { 3752 return (true); 3753 } 3754 }) 3755 ); 3756 } 3757 }); 3758 } else { 3759 // have to search the hard way (through all the data) 3760 for (u in records) { 3761 uuids.push(u); 3762 } 3763 // this is parallel! 3764 async.forEach(uuids, function (uuid, cb) { 3765 var vmobj = records[uuid]; 3766 var l_opts = {log: log}; 3767 3768 if (options.hasOwnProperty('fields') 3769 && need_fields.length > 0) { 3770 3771 // we have a specific set of fields we want to grab 3772 l_opts.fields = need_fields; 3773 } 3774 3775 loadVM(vmobj.uuid, data, l_opts, function (error, obj) { 3776 if (error) { 3777 if (error.code === 'ENOENT') { 3778 // zone likely was deleted since lookup, ignore 3779 cb(); 3780 } else { 3781 cb(error); 3782 } 3783 } else { 3784 if (transform) { 3785 transform(obj); 3786 } 3787 if (Object.keys(search).length === 0 3788 || matcher(obj, search)) { 3789 3790 results.push(obj); 3791 } 3792 cb(); 3793 } 3794 }); 3795 }, function (e) { 3796 var r; 3797 var short_results = []; 3798 3799 if (e) { 3800 callback(e); 3801 } else { 3802 if (options.full) { 3803 callback(null, results); 3804 } else if (options.fields && need_fields.length > 0) { 3805 if (options.hasOwnProperty('fields')) { 3806 filterFields(results); 3807 } 3808 callback(null, 3809 results.filter(function (res) { 3810 if (typeof (res) === 'object') { 3811 return (Object.keys(res).length > 0); 3812 } else { 3813 return (true); 3814 } 3815 }) 3816 ); 3817 } else { 3818 for (r in results) { 3819 short_results.push(results[r].uuid); 3820 } 3821 callback(null, short_results); 3822 } 3823 } 3824 }); 3825 } 3826 }); 3827 }; 3828 3829 // create a random new locally administered MAC address 3830 function generateMAC() 3831 { 3832 var data = [(Math.floor(Math.random() * 15) + 1).toString(16) + 2]; 3833 for (var i = 0; i < 5; i++) { 3834 var oct = (Math.floor(Math.random() * 255) + 1).toString(16); 3835 if (oct.length == 1) { 3836 oct = '0' + oct; 3837 } 3838 data.push(oct); 3839 } 3840 3841 return data.join(':'); 3842 } 3843 3844 // return the MAC address based on a VRRP Virtual Router ID 3845 function vrrpMAC(vrid) { 3846 return sprintf('00:00:5e:00:01:%02x', vrid); 3847 } 3848 3849 // Ensure we've got all the datasets necessary to create this VM 3850 // 3851 // IMPORTANT: 3852 // 3853 // On SmartOS, we assume a provisioner or some other external entity has already 3854 // loaded the dataset into the system. This function just confirms that the 3855 // dataset actually exists. 3856 // 3857 function checkDatasets(payload, log, callback) 3858 { 3859 var checkme = []; 3860 var d; 3861 var disk; 3862 3863 assert(log, 'no logger passed to checkDatasets()'); 3864 3865 log.debug('Checking for required datasets.'); 3866 3867 // build list of datasets we need to download (downloadme) 3868 for (disk in payload.add_disks) { 3869 if (payload.add_disks.hasOwnProperty(disk)) { 3870 d = payload.add_disks[disk]; 3871 if (d.hasOwnProperty('image_uuid')) { 3872 checkme.push(payload.zpool + '/' 3873 + d.image_uuid); 3874 } 3875 } 3876 } 3877 3878 function checker(dataset, cb) { 3879 zfs(['list', '-o', 'name', '-H', dataset], log, function (err, fds) { 3880 if (err) { 3881 log.error({'err': err, 'stdout': fds.stdout, 3882 'stderr': fds.stderr}, 'zfs list ' + dataset + ' ' 3883 + 'exited with' + ' code ' + err.code + ': ' + err.message); 3884 cb(new Error('unable to find dataset: ' + dataset)); 3885 } else { 3886 cb(); 3887 } 3888 }); 3889 } 3890 3891 // check that we have all the volumes 3892 async.forEachSeries(checkme, checker, function (err) { 3893 if (err) { 3894 log.error(err, 'checkDatasets() failed to find required ' 3895 + 'volumes'); 3896 callback(err); 3897 } else { 3898 // progress(100, 'we have all necessary datasets'); 3899 callback(); 3900 } 3901 }); 3902 } 3903 3904 function lookupConflicts(macs, ips, vrids, log, callback) { 3905 var conflict = false; 3906 var load_fields; 3907 3908 load_fields = ['brand', 'state', 'nics', 'uuid', 'zonename', 'zone_state']; 3909 3910 assert(log, 'no logger passed to lookupConflicts()'); 3911 3912 log.debug('checking for conflicts with ' 3913 + JSON.stringify(macs) + ', ' + JSON.stringify(ips) + ' and ' 3914 + JSON.stringify(vrids)); 3915 3916 if (macs.length === 0 && ips.length === 0 && vrids.length === 0) { 3917 log.debug('returning from conflict check (nothing to check)'); 3918 callback(null, conflict); 3919 return; 3920 } 3921 3922 preloadZoneData(null, {fields: load_fields, log: log}, 3923 function (err, data) { 3924 3925 var records = data.records; 3926 var uuid; 3927 var uuids = []; 3928 3929 if (err) { 3930 callback(err); 3931 return; 3932 } 3933 3934 for (uuid in records) { 3935 uuids.push(uuid); 3936 } 3937 3938 // this is parallel! 3939 async.forEach(uuids, function (z_uuid, cb) { 3940 loadVM(z_uuid, data, {fields: load_fields, log: log}, 3941 function (error, obj) { 3942 3943 var ip; 3944 var mac; 3945 var vrid; 3946 3947 if (error) { 3948 if (error.code === 'ENOENT') { 3949 // zone likely was deleted since lookup, ignore it 3950 cb(); 3951 } else { 3952 cb(error); 3953 } 3954 return; 3955 } 3956 3957 if (obj.state === 'failed' && obj.zone_state !== 'running') { 3958 // Ignore zones that are failed unless they're 'running' 3959 // which they shouldn't be because they get stopped on 3960 // failure. 3961 cb(); 3962 return; 3963 } 3964 3965 for (ip in ips) { 3966 if (ips[ip] !== 'dhcp' 3967 && matcher(obj, {'nics.*.ip': ips[ip]})) { 3968 3969 log.error('Found conflict: ' + obj.uuid 3970 + ' already has IP ' + ips[ip]); 3971 conflict = true; 3972 } 3973 } 3974 for (mac in macs) { 3975 if (matcher(obj, {'nics.*.mac': macs[mac]})) { 3976 log.error('Found conflict: ' + obj.uuid 3977 + ' already has MAC ' + macs[mac]); 3978 conflict = true; 3979 } 3980 } 3981 for (vrid in vrids) { 3982 if (matcher(obj, {'nics.*.vrrp_vrid': vrids[vrid]})) { 3983 log.error('Found conflict: ' + obj.uuid 3984 + ' already has VRID ' + vrids[vrid]); 3985 conflict = true; 3986 } 3987 } 3988 cb(); 3989 }); 3990 }, function (e) { 3991 if (e) { 3992 callback(e); 3993 } else { 3994 log.debug('returning from conflict check'); 3995 callback(null, conflict); 3996 } 3997 }); 3998 }); 3999 } 4000 4001 function lookupInvalidNicTags(nics, log, callback) { 4002 var etherstubs = []; 4003 var nic_tags = {}; 4004 4005 assert(log, 'no logger passed to lookupInvalidNicTags()'); 4006 4007 if (!nics || nics.length === 0) { 4008 callback(); 4009 return; 4010 } 4011 4012 async.parallel([ 4013 function (cb) { 4014 dladm.showEtherstub(null, log, function (err, stubs) { 4015 if (err) { 4016 cb(err); 4017 } else { 4018 etherstubs = stubs; 4019 cb(); 4020 } 4021 }); 4022 }, function (cb) { 4023 VM.getSysinfo([], {log: log}, function (err, sysinfo) { 4024 if (err) { 4025 cb(err); 4026 } else { 4027 var nic; 4028 var tag; 4029 for (nic in sysinfo['Network Interfaces']) { 4030 nic = sysinfo['Network Interfaces'][nic]; 4031 for (tag in nic['NIC Names']) { 4032 nic_tags[nic['NIC Names'][tag]] = 1; 4033 } 4034 } 4035 cb(); 4036 } 4037 }); 4038 } 4039 ], function (err, results) { 4040 if (err) { 4041 callback(err); 4042 return; 4043 } 4044 4045 var nic; 4046 for (nic in nics) { 4047 nic = nics[nic]; 4048 if (!nic.hasOwnProperty('nic_tag')) { 4049 continue; 4050 } 4051 if (!nic_tags.hasOwnProperty(nic.nic_tag) 4052 && (etherstubs.indexOf(nic.nic_tag) === -1)) { 4053 callback(new Error('Invalid nic tag "' + nic.nic_tag + '"')); 4054 return; 4055 } 4056 } 4057 4058 callback(); 4059 return; 4060 }); 4061 } 4062 4063 // create a new zvol for a VM 4064 function createVolume(volume, log, callback) 4065 { 4066 var refreserv; 4067 var size; 4068 var snapshot; 4069 4070 assert(log, 'no logger passed for createVolume()'); 4071 4072 log.debug('creating volume ' + JSON.stringify(volume)); 4073 4074 if (volume.hasOwnProperty('image_size')) { 4075 size = volume.image_size; 4076 } else if (volume.hasOwnProperty('size')) { 4077 size = volume.size; 4078 } else { 4079 callback(new Error('FATAL: createVolume(' + JSON.stringify(volume) 4080 + '): ' + 'has no size or image_size')); 4081 return; 4082 } 4083 4084 if (volume.hasOwnProperty('refreservation')) { 4085 refreserv = volume.refreservation; 4086 } else { 4087 log.debug('defaulting to refreservation = ' + size); 4088 refreserv = size; 4089 } 4090 4091 async.series([ 4092 function (cb) { 4093 if (volume.hasOwnProperty('image_uuid')) { 4094 snapshot = volume.zpool + '/' + volume.image_uuid + '@final'; 4095 zfs(['get', '-Ho', 'value', 'name', snapshot], log, 4096 function (err, fds) { 4097 4098 if (err) { 4099 if (fds.stderr.match('dataset does not exist')) { 4100 // no @final, so we'll make a new snapshot @<uuid> 4101 snapshot = volume.zpool + '/' + volume.image_uuid 4102 + '@' + volume.uuid; 4103 4104 zfs(['snapshot', snapshot], log, function (e) { 4105 cb(e); 4106 }); 4107 } else { 4108 cb(err); 4109 } 4110 } else { 4111 // @final is here! 4112 cb(); 4113 } 4114 }); 4115 } else { 4116 cb(); 4117 } 4118 }, function (cb) { 4119 var args; 4120 var target; 4121 4122 target = volume.zpool + '/' + volume.uuid; 4123 if (volume.hasOwnProperty('image_uuid')) { 4124 // This volume is from a template/dataset/image so we create 4125 // it as a clone of a the @final snapshot on the original. 4126 // we already set 'snapshot' to the correct location above. 4127 args = ['clone', '-F']; 4128 if (volume.hasOwnProperty('compression')) { 4129 args.push('-o', 'compression=' 4130 + volume.compression); 4131 } 4132 if (volume.hasOwnProperty('block_size')) { 4133 args.push('-o', 'volblocksize=' 4134 + volume.block_size); 4135 } 4136 args.push('-o', 'refreservation=' + refreserv + 'M'); 4137 args.push(snapshot, target); 4138 zfs(args, log, function (e) { 4139 if (e) { 4140 cb(e); 4141 } else { 4142 volume.path = '/dev/zvol/rdsk/' + target; 4143 cb(); 4144 } 4145 }); 4146 } else { 4147 // This volume is not from a template/dataset/image so we create 4148 // a blank new zvol for it. 4149 args = ['create']; 4150 if (volume.hasOwnProperty('compression')) { 4151 args.push('-o', 'compression=' 4152 + volume.compression); 4153 } 4154 if (volume.hasOwnProperty('block_size')) { 4155 args.push('-o', 'volblocksize=' 4156 + volume.block_size); 4157 } 4158 args.push('-o', 'refreservation=' + refreserv + 'M', '-V', 4159 size + 'M', target); 4160 zfs(args, log, function (err, fds) { 4161 if (err) { 4162 cb(err); 4163 } else { 4164 volume.path = '/dev/zvol/rdsk/' + target; 4165 cb(); 4166 } 4167 }); 4168 } 4169 } 4170 ], function (err, results) { 4171 callback(err); 4172 }); 4173 } 4174 4175 // Create all the volumes for a given VM property set 4176 function createVolumes(payload, log, callback) 4177 { 4178 var createme = []; 4179 var d; 4180 var disk; 4181 var disk_idx = 0; 4182 var used_disk_indexes = []; 4183 4184 assert(log, 'no logger passed to createVolumes()'); 4185 4186 log.debug('creating volumes: ' + JSON.stringify(payload.add_disks)); 4187 4188 if (payload.hasOwnProperty('used_disk_indexes')) { 4189 used_disk_indexes = payload.used_disk_indexes; 4190 } 4191 4192 for (disk in payload.add_disks) { 4193 if (payload.add_disks.hasOwnProperty(disk)) { 4194 d = payload.add_disks[disk]; 4195 4196 // we don't create CDROM devices or disk devices which have the 4197 // nocreate: true property. 4198 if (d.media !== 'cdrom' && !d.nocreate) { 4199 // skip to the next unused one. 4200 while (used_disk_indexes.indexOf(disk_idx) !== -1) { 4201 disk_idx++; 4202 } 4203 4204 d.index = disk_idx; 4205 d.uuid = payload.uuid + '-disk' + disk_idx; 4206 used_disk_indexes.push(Number(disk_idx)); 4207 if (!d.hasOwnProperty('zpool')) { 4208 d.zpool = payload.zpool; 4209 } 4210 createme.push(d); 4211 } 4212 } 4213 } 4214 4215 function loggedCreateVolume(volume, cb) { 4216 return createVolume(volume, log, cb); 4217 } 4218 4219 // create all the volumes we found that we need. 4220 async.forEachSeries(createme, loggedCreateVolume, function (err) { 4221 if (err) { 4222 callback(err); 4223 } else { 4224 callback(); 4225 } 4226 }); 4227 } 4228 4229 function writeAndRename(log, name, destfile, file_data, callback) 4230 { 4231 var tempfile = destfile + '.new'; 4232 4233 log.debug('writing ' + name + ' to ' + tempfile); 4234 4235 fs.writeFile(tempfile, file_data, function (err) { 4236 if (err) { 4237 callback(err); 4238 return; 4239 } 4240 4241 log.debug('wrote ' + name + ' to ' + tempfile); 4242 log.debug('renaming from ' + tempfile + ' to ' + destfile); 4243 4244 fs.rename(tempfile, destfile, function (_err) { 4245 if (_err) { 4246 callback(_err); 4247 return; 4248 } 4249 4250 log.debug('renamed from ' + tempfile + ' to ' + destfile); 4251 callback(); 4252 }); 4253 }); 4254 } 4255 4256 // writes a Zone's metadata JSON to /zones/<uuid>/config/metadata.json 4257 // and /zones/<uuid>/config/tags.json. 4258 function updateMetadata(vmobj, payload, log, callback) 4259 { 4260 var cmdata = {}; 4261 var imdata = {}; 4262 var key; 4263 var mdata = {}; 4264 var mdata_filename; 4265 var tags = {}; 4266 var tags_filename; 4267 var zonepath; 4268 4269 assert(log, 'no logger passed to updateMetadata()'); 4270 4271 if (vmobj.hasOwnProperty('zonepath')) { 4272 zonepath = vmobj.zonepath; 4273 } else if (vmobj.hasOwnProperty('zpool') 4274 && vmobj.hasOwnProperty('zonename')) { 4275 4276 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename; 4277 } else { 4278 callback(new Error('unable to find zonepath for ' 4279 + JSON.stringify(vmobj))); 4280 return; 4281 } 4282 4283 // paths are under zonepath but not zoneroot 4284 mdata_filename = zonepath + '/config/metadata.json'; 4285 tags_filename = zonepath + '/config/tags.json'; 4286 4287 // customer_metadata 4288 for (key in vmobj.customer_metadata) { 4289 if (vmobj.customer_metadata.hasOwnProperty(key)) { 4290 cmdata[key] = vmobj.customer_metadata[key]; 4291 if (payload.hasOwnProperty('remove_customer_metadata') 4292 && payload.remove_customer_metadata.indexOf(key) !== -1) { 4293 4294 // in the remove_* list, don't load it. 4295 delete cmdata[key]; 4296 } 4297 } 4298 } 4299 4300 for (key in payload.set_customer_metadata) { 4301 if (payload.set_customer_metadata.hasOwnProperty(key)) { 4302 cmdata[key] = payload.set_customer_metadata[key]; 4303 } 4304 } 4305 4306 // internal_metadata 4307 for (key in vmobj.internal_metadata) { 4308 if (vmobj.internal_metadata.hasOwnProperty(key)) { 4309 imdata[key] = vmobj.internal_metadata[key]; 4310 if (payload.hasOwnProperty('remove_internal_metadata') 4311 && payload.remove_internal_metadata.indexOf(key) !== -1) { 4312 4313 // in the remove_* list, don't load it. 4314 delete imdata[key]; 4315 } 4316 } 4317 } 4318 4319 for (key in payload.set_internal_metadata) { 4320 if (payload.set_internal_metadata.hasOwnProperty(key)) { 4321 imdata[key] = payload.set_internal_metadata[key]; 4322 } 4323 } 4324 4325 // same thing for tags 4326 for (key in vmobj.tags) { 4327 if (vmobj.tags.hasOwnProperty(key)) { 4328 tags[key] = vmobj.tags[key]; 4329 if (payload.hasOwnProperty('remove_tags') 4330 && payload.remove_tags.indexOf(key) !== -1) { 4331 4332 // in the remove_* list, don't load it. 4333 delete tags[key]; 4334 } 4335 } 4336 } 4337 4338 for (key in payload.set_tags) { 4339 if (payload.set_tags.hasOwnProperty(key)) { 4340 tags[key] = payload.set_tags[key]; 4341 } 4342 } 4343 4344 mdata = {'customer_metadata': cmdata, 'internal_metadata': imdata}; 4345 4346 async.series([ 4347 function (next) { 4348 writeAndRename(log, 'metadata', mdata_filename, 4349 JSON.stringify(mdata, null, 2), next); 4350 }, 4351 function (next) { 4352 writeAndRename(log, 'tags', tags_filename, 4353 JSON.stringify(tags, null, 2), next); 4354 } 4355 ], callback); 4356 } 4357 4358 function saveMetadata(payload, log, callback) 4359 { 4360 var protovm = {}; 4361 4362 assert(log, 'no logger passed to saveMetadata()'); 4363 4364 if (!payload.hasOwnProperty('zonepath') 4365 || !payload.hasOwnProperty('zpool') 4366 || !payload.hasOwnProperty('zonename')) { 4367 4368 callback(new Error('saveMetadata payload is missing zone ' 4369 + 'properties.')); 4370 return; 4371 } 4372 4373 protovm.zonepath = payload.zonepath; 4374 protovm.zpool = payload.zpool; 4375 protovm.zonename = payload.zonename; 4376 protovm.customer_metadata = {}; 4377 protovm.tags = {}; 4378 4379 if (payload.hasOwnProperty('tags')) { 4380 payload.set_tags = payload.tags; 4381 delete payload.tags; 4382 } 4383 if (payload.hasOwnProperty('customer_metadata')) { 4384 payload.set_customer_metadata = payload.customer_metadata; 4385 delete payload.customer_metadata; 4386 } 4387 if (payload.hasOwnProperty('internal_metadata')) { 4388 payload.set_internal_metadata = payload.internal_metadata; 4389 delete payload.internal_metadata; 4390 } 4391 4392 updateMetadata(protovm, payload, log, callback); 4393 } 4394 4395 // writes a zone's metadata JSON to /zones/<uuid>/config/routes.json 4396 function updateRoutes(vmobj, payload, log, callback) 4397 { 4398 var filename; 4399 var key; 4400 var routes = {}; 4401 var zonepath; 4402 4403 assert(log, 'no logger passed to updateRoutes()'); 4404 4405 if (vmobj.hasOwnProperty('zonepath')) { 4406 zonepath = vmobj.zonepath; 4407 } else if (vmobj.hasOwnProperty('zpool') 4408 && vmobj.hasOwnProperty('zonename')) { 4409 4410 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename; 4411 } else { 4412 callback(new Error('unable to find zonepath for ' 4413 + JSON.stringify(vmobj))); 4414 return; 4415 } 4416 4417 // paths are under zonepath but not zoneroot 4418 filename = zonepath + '/config/routes.json'; 4419 4420 for (key in vmobj.routes) { 4421 if (vmobj.routes.hasOwnProperty(key)) { 4422 routes[key] = vmobj.routes[key]; 4423 if (payload.hasOwnProperty('remove_routes') 4424 && payload.remove_routes.indexOf(key) !== -1) { 4425 4426 // in the remove_* list, don't load it. 4427 delete routes[key]; 4428 } 4429 } 4430 } 4431 4432 for (key in payload.set_routes) { 4433 if (payload.set_routes.hasOwnProperty(key)) { 4434 routes[key] = payload.set_routes[key]; 4435 } 4436 } 4437 4438 fs.writeFile(filename, JSON.stringify(routes, null, 2), 4439 function (err) { 4440 if (err) { 4441 callback(err); 4442 } else { 4443 log.debug('wrote routes to ' + filename); 4444 callback(); 4445 } 4446 }); 4447 } 4448 4449 function saveRoutes(payload, log, callback) 4450 { 4451 var protovm = {}; 4452 4453 assert(log, 'no logger passed to saveRoutes()'); 4454 4455 if (!payload.hasOwnProperty('zonepath') 4456 || !payload.hasOwnProperty('zpool') 4457 || !payload.hasOwnProperty('zonename')) { 4458 4459 callback(new Error('saveRoutes payload is missing zone ' 4460 + 'properties.')); 4461 return; 4462 } 4463 4464 protovm.zonepath = payload.zonepath; 4465 protovm.zpool = payload.zpool; 4466 protovm.zonename = payload.zonename; 4467 4468 if (payload.hasOwnProperty('routes')) { 4469 payload.set_routes = payload.routes; 4470 delete payload.routes; 4471 } 4472 4473 updateRoutes(protovm, payload, log, callback); 4474 } 4475 4476 function createVM(payload, log, callback) 4477 { 4478 assert(log, 'no logger passed to createVM()'); 4479 4480 async.series([ 4481 function (cb) { 4482 if (!payload.create_only) { 4483 // progress(2, 'checking required datasets'); 4484 checkDatasets(payload, log, cb); 4485 } else { 4486 cb(); 4487 } 4488 }, function (cb) { 4489 if (!payload.create_only) { 4490 // progress(29, 'creating volumes'); 4491 createVolumes(payload, log, cb); 4492 } else { 4493 cb(); 4494 } 4495 }, function (cb) { 4496 // progress(51, 'creating zone container'); 4497 createZone(payload, log, cb); 4498 } 4499 ], function (err, results) { 4500 if (err) { 4501 callback(err); 4502 } else { 4503 callback(null, results); 4504 } 4505 }); 4506 } 4507 4508 function fixZoneinitMetadataSock(zoneroot, log, callback) 4509 { 4510 var mdata_00; 4511 4512 // ensure we're safe to touch these files, zone should not be running here 4513 // so this just guards against malicious datasets. 4514 ['/var/zoneinit/includes', '/root/zoneinit.d'].forEach(function (dir) { 4515 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true}); 4516 }); 4517 4518 function replaceData(filename, cb) { 4519 fs.readFile(filename, 'utf8', function (error, data) { 4520 if (error) { 4521 log.error(error, 'failed to load 00-mdata.sh for replacement'); 4522 cb(error); 4523 return; 4524 } 4525 4526 data = data.replace(/\/var\/run\/smartdc\/metadata.sock/g, 4527 '/.zonecontrol/metadata.sock'); 4528 4529 log.trace('writing [' + data + '] to ' + filename); 4530 fs.writeFile(filename, data, 'utf8', function (err) { 4531 if (err) { 4532 log.error(err, 'failed to write ' + filename); 4533 } 4534 cb(err); 4535 }); 4536 }); 4537 } 4538 4539 // try /var/zoneinit/includes/00-mdata.sh first, since that's in new images 4540 mdata_00 = path.join(zoneroot, '/var/zoneinit/includes/00-mdata.sh'); 4541 fs.exists(mdata_00, function (exists1) { 4542 if (exists1) { 4543 log.info('fixing socket in /var/zoneinit/includes/00-mdata.sh'); 4544 replaceData(mdata_00, callback); 4545 } else { 4546 // didn't exist, so try location it exists in older images eg. 1.6.3 4547 mdata_00 = path.join(zoneroot, '/root/zoneinit.d/00-mdata.sh'); 4548 fs.exists(mdata_00, function (exists2) { 4549 if (exists2) { 4550 log.info('fixing socket in /root/zoneinit.d/00-mdata.sh'); 4551 replaceData(mdata_00, callback); 4552 } else { 4553 log.info('no 00-mdata.sh to cleanup in zoneinit'); 4554 callback(); 4555 } 4556 }); 4557 } 4558 }); 4559 } 4560 4561 function fixMdataFetchStart(zonepath, log, callback) 4562 { 4563 // svccfg validates zonepath 4564 var mdata_fetch_start = '/lib/svc/method/mdata-fetch'; 4565 4566 svccfg(zonepath, ['-s', 'svc:/smartdc/mdata:fetch', 'setprop', 'start/exec', 4567 '=', mdata_fetch_start], log, function (error, stdio) { 4568 4569 if (error) { 4570 log.error(error, 'failed to set mdata:fetch start method'); 4571 } else { 4572 log.info('successfully set mdata:fetch start method'); 4573 } 4574 4575 callback(error); 4576 }); 4577 } 4578 4579 function cleanupMessyDataset(zonepath, brand, log, callback) 4580 { 4581 var command; 4582 var zoneroot = path.join(zonepath, '/root'); 4583 4584 assert(log, 'no logger passed to cleanupMessyDataset()'); 4585 4586 try { 4587 ['/var/adm', '/var/svc/log', '/var/svc/manifest', '/root/zoneinit.d'] 4588 .forEach(function (dir) { 4589 4590 // This will ensure these are safe if they exist. 4591 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true}); 4592 }); 4593 } catch (e) { 4594 log.error(e, 'Unable to cleanup dataset: ' + e.message); 4595 callback(e); 4596 return; 4597 } 4598 4599 // We've verified the directories here exist, and have no symlinks in the 4600 // path (or don't exist) so rm -f <dir>/<file> should be safe regardless of 4601 // the type of <file> 4602 4603 command = 'rm -f ' 4604 + zoneroot + '/var/adm/utmpx ' 4605 + zoneroot + '/var/adm/wtmpx ' 4606 + zoneroot + '/var/svc/log/*.log ' 4607 + zoneroot + '/var/svc/mdata ' 4608 + zoneroot + '/var/svc/manifest/mdata.xml '; 4609 4610 if (! BRAND_OPTIONS[brand].features.zoneinit) { 4611 // eg. joyent-minimal (don't need zoneinit) 4612 command = command + zoneroot + '/root/zoneinit.xml ' 4613 + zoneroot + '/root/zoneinit ' 4614 + '&& rm -rf ' + zoneroot + '/root/zoneinit.d '; 4615 } 4616 4617 command = command + '&& touch ' + zoneroot + '/var/adm/wtmpx'; 4618 log.debug(command); 4619 exec(command, function (error, stdout, stderr) { 4620 log.debug({err: error, stdout: stdout, stderr: stderr}, 4621 'returned from cleaning up dataset'); 4622 if (error || !BRAND_OPTIONS[brand].features.zoneinit) { 4623 // either we already failed or this zone doesn't use zoneinit so 4624 // we don't need to bother fixing zoneinit's scripts. 4625 callback(error); 4626 } else { 4627 fixZoneinitMetadataSock(zoneroot, log, function (err) { 4628 // See OS-2314, currently we assume all zones w/ zoneinit also 4629 // have broken mdata:fetch when images are created from them. 4630 // Attempt to fix that too. 4631 fixMdataFetchStart(zonepath, log, callback); 4632 }); 4633 } 4634 }); 4635 } 4636 4637 // Helper for unlinking and replacing a file that you've already confirmed 4638 // has no symlinks. Throws error when fs.writeFileSync does, or when 4639 // fs.unlinkSync throws non ENOENT. 4640 function replaceFile(zoneroot, filename, data) { 4641 // first delete, in case file itself is a link 4642 try { 4643 fs.unlinkSync(path.join(zoneroot, filename)); 4644 } catch (e) { 4645 if (e.code !== 'ENOENT') { 4646 throw e; 4647 } 4648 } 4649 4650 fs.writeFileSync(path.join(zoneroot, filename), data); 4651 } 4652 4653 // NOTE: we write these out initially before the zone is started, but after that 4654 // rely on mdata-fetch in the zone to do the updates since we can't safely write 4655 // these files in the zones. 4656 function writeZoneNetfiles(payload, log, callback) 4657 { 4658 var hostname; 4659 var n; 4660 var nic; 4661 var primary_found = false; 4662 var zoneroot; 4663 4664 assert(log, 'no logger passed to writeZoneNetfiles()'); 4665 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload'); 4666 4667 zoneroot = payload.zonepath + '/root'; 4668 4669 try { 4670 assertSafeZonePath(zoneroot, '/etc', {type: 'dir', enoent_ok: true}); 4671 } catch (e) { 4672 log.error(e, 'Unable to write zone net files: ' + e.message); 4673 callback(e); 4674 return; 4675 } 4676 4677 log.info('Writing network files to zone root'); 4678 4679 try { 4680 for (nic in payload.add_nics) { 4681 if (payload.add_nics.hasOwnProperty(nic)) { 4682 n = payload.add_nics[nic]; 4683 4684 if (n.ip != 'dhcp') { 4685 replaceFile(zoneroot, '/etc/hostname.' 4686 + n.interface, n.ip + ' netmask ' + n.netmask 4687 + ' up' + '\n'); 4688 } 4689 4690 if (n.hasOwnProperty('primary') && !primary_found) { 4691 // only allow one primary network 4692 primary_found = true; 4693 if (n.hasOwnProperty('gateway')) { 4694 replaceFile(zoneroot, '/etc/defaultrouter', 4695 n.gateway + '\n'); 4696 } 4697 if (n.ip == 'dhcp') { 4698 replaceFile(zoneroot, '/etc/dhcp.' + n.interface, ''); 4699 } 4700 } 4701 } 4702 } 4703 4704 // It's possible we don't have zonename or hostname set because of the 4705 // ordering of adding the UUID. In any case, we'll have at least a uuid 4706 // here. 4707 if (payload.hasOwnProperty('hostname')) { 4708 hostname = payload.hostname; 4709 } else if (payload.hasOwnProperty('zonename')) { 4710 hostname = payload.zonename; 4711 } else { 4712 hostname = payload.uuid; 4713 } 4714 4715 replaceFile(zoneroot, '/etc/nodename', hostname + '\n'); 4716 } catch (e) { 4717 log.error(e, 'Unable to write zone networking files: ' + e.message); 4718 callback(e); 4719 return; 4720 } 4721 4722 callback(); 4723 } 4724 4725 /* 4726 * NOTE: once we no longer support old datasets that need the 'zoneconfig' file, 4727 * this function and calls to it can be removed. 4728 * 4729 * This writes out the zoneconfig file that is used by the zoneinit service in 4730 * joyent branded zones' datasets. 4731 * 4732 */ 4733 function writeZoneconfig(payload, log, callback) 4734 { 4735 var data; 4736 var hostname; 4737 var n; 4738 var nic; 4739 var zoneroot; 4740 4741 assert(log, 'no logger passed to writeZoneconfig()'); 4742 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload'); 4743 4744 zoneroot = payload.zonepath + '/root'; 4745 4746 log.info('Writing config for zoneinit'); 4747 4748 if (payload.hasOwnProperty('hostname')) { 4749 hostname = payload.hostname; 4750 } else { 4751 hostname = payload.zonename; 4752 } 4753 4754 data = 'TEMPLATE_VERSION=0.0.1\n' 4755 + 'ZONENAME=' + payload.zonename + '\n' 4756 + 'HOSTNAME=' + hostname + '.' + payload.dns_domain + '\n' 4757 + 'TMPFS=' + payload.tmpfs + 'm\n'; 4758 4759 if (payload.hasOwnProperty('add_nics') && payload.add_nics[0]) { 4760 4761 if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') { 4762 data = data + 'PUBLIC_IP=' + payload.add_nics[0].ip + '\n'; 4763 } 4764 if (payload.add_nics[1] && payload.add_nics[1].ip != 'dhcp') { 4765 data = data + 'PRIVATE_IP=' + payload.add_nics[1].ip + '\n'; 4766 } else if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') { 4767 // zoneinit uses private_ip for /etc/hosts, we want to 4768 // make that same as public, if there's no actual private. 4769 data = data + 'PRIVATE_IP=' + payload.add_nics[0].ip + '\n'; 4770 } 4771 } 4772 4773 if (payload.hasOwnProperty('resolvers')) { 4774 // zoneinit appends to resolv.conf rather than overwriting, so just 4775 // add to the zoneconfig and let zoneinit handle it 4776 data = data + 'RESOLVERS="' + payload.resolvers.join(' ') + '"\n'; 4777 } 4778 4779 for (nic in payload.add_nics) { 4780 if (payload.add_nics.hasOwnProperty(nic)) { 4781 n = payload.add_nics[nic]; 4782 data = data + n.interface.toUpperCase() + '_MAC=' + n.mac + '\n' 4783 + n.interface.toUpperCase() + '_INTERFACE=' 4784 + n.interface.toUpperCase() + '\n'; 4785 4786 if (n.ip != 'dhcp') { 4787 data = data + n.interface.toUpperCase() + '_IP=' + n.ip + '\n' 4788 + n.interface.toUpperCase() + '_NETMASK=' 4789 + n.netmask + '\n'; 4790 } 4791 } 4792 } 4793 4794 try { 4795 assertSafeZonePath(zoneroot, '/var/svc/log/system-zoneinit:default.log', 4796 {type: 'file', enoent_ok: true}); 4797 assertSafeZonePath(zoneroot, '/root/zoneconfig', 4798 {type: 'file', enoent_ok: true}); 4799 4800 replaceFile(zoneroot, '/var/svc/log/system-zoneinit:default.log', ''); 4801 4802 log.debug('writing zoneconfig ' + JSON.stringify(data) + ' to ' 4803 + zoneroot); 4804 4805 replaceFile(zoneroot, '/root/zoneconfig', data); 4806 callback(); 4807 } catch (e) { 4808 log.error(e, 'Unable to write zoneconfig files: ' + e.message); 4809 callback(e); 4810 return; 4811 } 4812 } 4813 4814 function zonecfg(args, log, callback) 4815 { 4816 var cmd = '/usr/sbin/zonecfg'; 4817 4818 assert(log, 'no logger passed to zonecfg()'); 4819 4820 log.debug(cmd + ' ' + args.join(' ')); 4821 execFile(cmd, args, function (error, stdout, stderr) { 4822 if (error) { 4823 callback(error, {'stdout': stdout, 'stderr': stderr}); 4824 } else { 4825 callback(null, {'stdout': stdout, 'stderr': stderr}); 4826 } 4827 }); 4828 } 4829 4830 function zonecfgFile(data, args, log, callback) 4831 { 4832 var tmpfile = '/tmp/zonecfg.' + process.pid + '.tmp'; 4833 4834 assert(log, 'no logger passed to zonecfgFile()'); 4835 4836 fs.writeFile(tmpfile, data, function (err, result) { 4837 if (err) { 4838 // On failure we don't delete the tmpfile so we can debug it. 4839 callback(err); 4840 } else { 4841 args.push('-f'); 4842 args.push(tmpfile); 4843 4844 zonecfg(args, log, function (e, fds) { 4845 if (e) { 4846 // keep temp file around for investigation 4847 callback(e, fds); 4848 } else { 4849 fs.unlink(tmpfile, function () { 4850 callback(null, fds); 4851 }); 4852 } 4853 }); 4854 } 4855 }); 4856 } 4857 4858 function zoneadm(args, log, callback) 4859 { 4860 var cmd = '/usr/sbin/zoneadm'; 4861 4862 assert(log, 'no logger passed to zoneadm()'); 4863 4864 log.debug(cmd + ' ' + args.join(' ')); 4865 execFile(cmd, args, function (error, stdout, stderr) { 4866 if (error) { 4867 callback(error, {'stdout': stdout, 'stderr': stderr}); 4868 } else { 4869 callback(null, {'stdout': stdout, 'stderr': stderr}); 4870 } 4871 }); 4872 } 4873 4874 function zfs(args, log, callback) 4875 { 4876 var cmd = '/usr/sbin/zfs'; 4877 4878 assert(log, 'no logger passed to zfs()'); 4879 4880 log.debug(cmd + ' ' + args.join(' ')); 4881 execFile(cmd, args, function (error, stdout, stderr) { 4882 if (error) { 4883 callback(error, {'stdout': stdout, 'stderr': stderr}); 4884 } else { 4885 callback(null, {'stdout': stdout, 'stderr': stderr}); 4886 } 4887 }); 4888 } 4889 4890 exports.getSysinfo = function (args, options, callback) 4891 { 4892 var cmd = '/usr/bin/sysinfo'; 4893 var log; 4894 4895 // we used to allow just one argument (callback) and we also allow 2 args 4896 // (args, callback) so that options is optional. 4897 if (arguments.length === 1) { 4898 callback = arguments[0]; 4899 args = []; 4900 options = {}; 4901 } 4902 if (arguments.length === 2) { 4903 callback = arguments[1]; 4904 options = {}; 4905 } 4906 4907 ensureLogging(false); 4908 if (options.hasOwnProperty('log')) { 4909 log = options.log; 4910 } else { 4911 log = VM.log.child({action: 'getSysinfo'}); 4912 } 4913 4914 log.debug(cmd + ' ' + args.join(' ')); 4915 execFile(cmd, args, function (error, stdout, stderr) { 4916 var sysinfo; 4917 4918 if (error) { 4919 callback(error, {'stdout': stdout, 'stderr': stderr}); 4920 } else { 4921 try { 4922 sysinfo = JSON.parse(stdout.toString()); 4923 } catch (e) { 4924 sysinfo = {}; 4925 } 4926 callback(null, sysinfo); 4927 } 4928 }); 4929 }; 4930 4931 /* 4932 * This watches zone transitions and calls callback when specified 4933 * state is reached. Optionally you can set a timeout which will 4934 * call your callback when the timeout occurs whether the transition 4935 * has happened or not. 4936 * 4937 * payload needs to have at least .zonename and .uuid 4938 * 4939 */ 4940 exports.waitForZoneState = function (payload, state, options, callback) 4941 { 4942 var log; 4943 var sysevent_state; 4944 var timeout; 4945 var timeout_secs = PROVISION_TIMEOUT; 4946 var watcher; 4947 4948 // options is optional 4949 if (arguments.length === 3) { 4950 callback = arguments[2]; 4951 options = {}; 4952 } 4953 4954 ensureLogging(false); 4955 if (options.hasOwnProperty('log')) { 4956 log = options.log; 4957 } else { 4958 log = VM.log.child({action: 'waitForZoneState', vm: payload.uuid}); 4959 } 4960 4961 if (options.hasOwnProperty('timeout')) { 4962 timeout_secs = options.timeout; 4963 } 4964 4965 sysevent_state = state; 4966 if (state === 'installed') { 4967 // Apparently the zone status 'installed' equals sysevent status 4968 // 'uninitialized' 4969 sysevent_state = 'uninitialized'; 4970 } 4971 4972 function done() { 4973 if (timeout) { 4974 clearTimeout(timeout); 4975 timeout = null; 4976 } 4977 } 4978 4979 function handler(err, obj) { 4980 if (err) { 4981 done(); 4982 callback(err); 4983 return; 4984 } 4985 log.trace('handler got: ' + JSON.stringify(obj)); 4986 if (obj.zonename !== payload.zonename) { 4987 return; 4988 } 4989 4990 if (obj.newstate === sysevent_state) { 4991 // Load again to confirm 4992 VM.lookup({'zonename': obj.zonename}, 4993 {fields: ['zone_state'], log: log}, 4994 function (error, res) { 4995 var handler_retry; 4996 4997 if (error) { 4998 watcher.cleanup(); 4999 done(); 5000 callback(error); 5001 return; 5002 } 5003 5004 if (res.length !== 1) { 5005 watcher.cleanup(); 5006 done(); 5007 callback(new Error('lookup could no find VM ' 5008 + obj.zonename)); 5009 return; 5010 } 5011 5012 if (res[0].hasOwnProperty('zone_state') 5013 && res[0].zone_state === state) { 5014 5015 // found the state we're looking for, success! 5016 log.debug('saw zone go to ' + obj.newstate + ' (' 5017 + state + ') calling callback()'); 5018 watcher.cleanup(); 5019 done(); 5020 callback(); 5021 } else if (timeout) { 5022 // we saw a state change to a state we don't care about 5023 // so if we've not timed out try reloading again in a 5024 // second. 5025 if (!handler_retry) { 5026 handler_retry = setTimeout(function () { 5027 if (timeout) { 5028 // try again if wait timeout is still set 5029 handler(null, obj); 5030 } 5031 handler_retry = null; 5032 }, 1000); 5033 log.debug('zone state after lookup: ' 5034 + res[0].zone_state + ', still waiting'); 5035 } else { 5036 log.debug('zone in wrong state but we already' 5037 + ' have a handler running'); 5038 } 5039 } else { 5040 // no timeout set and we're not at the correct state 5041 log.error('failed to reach state: ' + state); 5042 callback(new Error('failed to reach state: ' + state)); 5043 } 5044 } 5045 ); 5046 } 5047 } 5048 5049 watcher = watchZoneTransitions(handler, log); 5050 5051 timeout = setTimeout(function () { 5052 var err; 5053 5054 done(); 5055 watcher.cleanup(); 5056 err = new Error('timed out waiting for zone to transition to ' + state); 5057 err.code = 'ETIMEOUT'; 5058 callback(err); 5059 }, timeout_secs * 1000); 5060 5061 // after we've started the watcher (if we checked before there'd be a race) 5062 // we check whether we're already in the target state, if we are close it 5063 // down and return. 5064 VM.load(payload.uuid, {fields: ['zone_state'], log: log}, 5065 function (err, obj) { 5066 5067 if (err) { 5068 watcher.cleanup(); 5069 done(); 5070 callback(err); 5071 } else if (obj.hasOwnProperty('zone_state') 5072 && obj.zone_state === state) { 5073 5074 watcher.cleanup(); 5075 done(); 5076 log.info('VM is in state ' + state); 5077 callback(); // at correct state! 5078 } 5079 }); 5080 }; 5081 5082 // handler() will be called with an object describing the transition for any 5083 // transitions seen (after any filtering). The only filtering here is to remove 5084 // duplicate events. Other filtering should be done by the caller. 5085 function watchZoneTransitions(handler, log) { 5086 var buffer = ''; 5087 var chunks; 5088 var cleanup; 5089 var watcher; 5090 var watcher_pid; 5091 5092 assert(log, 'no logger passed to watchZoneTransitions()'); 5093 5094 if (!zoneevent) { 5095 5096 zoneevent = new EventEmitter(); 5097 5098 log.debug('/usr/vm/sbin/zoneevent'); 5099 watcher = spawn('/usr/vm/sbin/zoneevent', [], 5100 {'customFds': [-1, -1, -1]}); 5101 log.debug('zoneevent running with pid ' + watcher.pid); 5102 watcher_pid = watcher.pid; 5103 5104 watcher.stdout.on('data', function (data) { 5105 var chunk; 5106 var obj; 5107 var prev_msg; 5108 5109 buffer += data.toString(); 5110 chunks = buffer.split('\n'); 5111 while (chunks.length > 1) { 5112 chunk = chunks.shift(); 5113 obj = JSON.parse(chunk); 5114 5115 if (obj === prev_msg) { 5116 // Note: sometimes sysevent emits multiple events for the 5117 // same status, we only want the first one here because just 5118 // because sysevent does it, doesn't make it right. 5119 log.debug('duplicate zoneevent message! ' 5120 + JSON.stringify(obj)); 5121 } else if (zoneevent) { 5122 zoneevent.emit('zoneevent', null, obj); 5123 } 5124 } 5125 buffer = chunks.pop(); 5126 }); 5127 5128 // doesn't take input. 5129 watcher.stdin.end(); 5130 5131 watcher.on('exit', function (code) { 5132 log.warn('zoneevent watcher ' + watcher_pid + ' exited: ', 5133 JSON.stringify(code)); 5134 // tell all the listeners of this zoneevent (if there are any) that 5135 // we exited. Then null it out so next time we'll make a new one. 5136 zoneevent.emit('zoneevent', new Error('zoneevent watcher exited ' 5137 + 'prematurely with code: ' + code)); 5138 zoneevent = null; 5139 }); 5140 } 5141 5142 cleanup = function () { 5143 var listeners; 5144 5145 if (zoneevent) { 5146 listeners = zoneevent.listeners('zoneevent'); 5147 5148 log.debug('cleanup called w/ listeners: ' 5149 + util.inspect(listeners)); 5150 zoneevent.removeListener('zoneevent', handler); 5151 if (zoneevent.listeners('zoneevent').length === 0) { 5152 log.debug('zoneevent watcher ' + watcher_pid 5153 + ' cleanup called'); 5154 zoneevent = null; 5155 if (watcher) { 5156 watcher.stdout.destroy(); // so we don't send more 'data' 5157 watcher.stderr.destroy(); 5158 watcher.removeAllListeners('exit'); // so don't fail on kill 5159 watcher.kill(); 5160 watcher = null; 5161 } 5162 } 5163 } else if (watcher) { 5164 watcher.stdout.destroy(); // so we don't send more 'data' 5165 watcher.stderr.destroy(); 5166 watcher.removeAllListeners('exit'); // so don't fail on our kill 5167 watcher.kill(); 5168 watcher = null; 5169 } 5170 }; 5171 5172 zoneevent.on('zoneevent', handler); 5173 5174 return ({'cleanup': cleanup}); 5175 } 5176 5177 function fixPayloadMemory(payload, vmobj, log) 5178 { 5179 var brand; 5180 var max_locked; 5181 var max_phys; 5182 var min_overhead; 5183 var ram; 5184 5185 assert(log, 'no logger passed to fixPayloadMemory()'); 5186 5187 if (vmobj.hasOwnProperty('brand')) { 5188 brand = vmobj.brand; 5189 } else if (payload.hasOwnProperty('brand')) { 5190 brand = payload.brand; 5191 } 5192 5193 if (BRAND_OPTIONS[brand].features.default_memory_overhead 5194 && payload.hasOwnProperty('ram') 5195 && !payload.hasOwnProperty('max_physical_memory')) { 5196 5197 // For now we add overhead to the memory caps for KVM zones, this 5198 // is for the qemu process itself. Since customers don't have direct 5199 // access to zone memory, this exists mostly to protect against bugs. 5200 payload.max_physical_memory = (payload.ram 5201 + BRAND_OPTIONS[brand].features.default_memory_overhead); 5202 } else if (payload.hasOwnProperty('ram') 5203 && !payload.hasOwnProperty('max_physical_memory')) { 5204 5205 payload.max_physical_memory = payload.ram; 5206 } 5207 5208 if (payload.hasOwnProperty('max_physical_memory')) { 5209 if (!payload.hasOwnProperty('max_locked_memory')) { 5210 if (vmobj.hasOwnProperty('max_locked_memory') 5211 && vmobj.hasOwnProperty('max_physical_memory')) { 5212 5213 // we don't have a new value, so first try to keep the same 5214 // delta that existed before btw. max_phys and max_locked 5215 payload.max_locked_memory = payload.max_physical_memory 5216 - (vmobj.max_physical_memory - vmobj.max_locked_memory); 5217 } else { 5218 // existing obj doesn't have max_locked, add one now 5219 payload.max_locked_memory = payload.max_physical_memory; 5220 } 5221 } 5222 5223 if (!payload.hasOwnProperty('max_swap')) { 5224 if (vmobj.hasOwnProperty('max_swap') 5225 && vmobj.hasOwnProperty('max_physical_memory')) { 5226 5227 // we don't have a new value, so first try to keep the same 5228 // delta that existed before btw. max_phys and max_swap 5229 if (vmobj.max_swap === MINIMUM_MAX_SWAP 5230 && vmobj.max_swap <= MINIMUM_MAX_SWAP 5231 && payload.max_physical_memory >= MINIMUM_MAX_SWAP) { 5232 // in this case we artificially inflated before to meet 5233 // minimum tie back to ram. 5234 payload.max_swap = payload.max_physical_memory; 5235 } else { 5236 payload.max_swap = payload.max_physical_memory 5237 + (vmobj.max_swap - vmobj.max_physical_memory); 5238 } 5239 } else { 5240 // existing obj doesn't have max_swap, add one now 5241 payload.max_swap = payload.max_physical_memory; 5242 } 5243 5244 // never add a max_swap less than MINIMUM_MAX_SWAP 5245 if (payload.max_swap < MINIMUM_MAX_SWAP) { 5246 payload.max_swap = MINIMUM_MAX_SWAP; 5247 } 5248 } 5249 } 5250 5251 // if we're updating tmpfs it must be lower than our new max_physical or 5252 // if we're not also changing max_physical, it must be lower than the 5253 // current one. 5254 if (payload.hasOwnProperty('tmpfs')) { 5255 if (payload.hasOwnProperty('max_physical_memory') 5256 && (Number(payload.tmpfs) 5257 > Number(payload.max_physical_memory))) { 5258 5259 payload.tmpfs = payload.max_physical_memory; 5260 } else if (Number(payload.tmpfs) 5261 > Number(vmobj.max_physical_memory)) { 5262 5263 payload.tmpfs = vmobj.max_physical_memory; 5264 } 5265 } 5266 5267 if (payload.hasOwnProperty('max_physical_memory') 5268 && BRAND_OPTIONS[brand].features.use_tmpfs 5269 && !payload.hasOwnProperty('tmpfs')) { 5270 5271 if (vmobj.hasOwnProperty('max_physical_memory') 5272 && vmobj.hasOwnProperty('tmpfs')) { 5273 5274 // change tmpfs to be the same ratio of ram as before 5275 payload.tmpfs = ((vmobj.tmpfs / vmobj.max_physical_memory) 5276 * payload.max_physical_memory); 5277 payload.tmpfs = Number(payload.tmpfs).toFixed(); 5278 } else { 5279 // tmpfs must be < max_physical_memory, if not: pretend it was 5280 payload.tmpfs = payload.max_physical_memory; 5281 } 5282 } 5283 5284 // now that we've possibly adjusted target values, lower/raise values to 5285 // satisify max/min. 5286 5287 min_overhead = BRAND_OPTIONS[brand].features.min_memory_overhead; 5288 if (min_overhead) { 5289 ram = payload.hasOwnProperty('ram') ? payload.ram : vmobj.ram; 5290 max_phys = payload.hasOwnProperty('max_physical_memory') 5291 ? payload.max_physical_memory : vmobj.max_physical_memory; 5292 max_locked = payload.hasOwnProperty('max_locked_memory') 5293 ? payload.max_locked_memory : vmobj.max_locked_memory; 5294 5295 if ((ram + min_overhead) > max_phys) { 5296 payload.max_physical_memory = (ram + min_overhead); 5297 } 5298 if ((ram + min_overhead) > max_locked) { 5299 payload.max_locked_memory = (ram + min_overhead); 5300 } 5301 } 5302 5303 if (payload.hasOwnProperty('max_locked_memory')) { 5304 if (payload.hasOwnProperty('max_physical_memory')) { 5305 if (payload.max_locked_memory > payload.max_physical_memory) { 5306 log.warn('max_locked_memory (' + payload.max_locked_memory 5307 + ') > max_physical_memory (' + payload.max_physical_memory 5308 + ') clamping to ' + payload.max_physical_memory); 5309 payload.max_locked_memory = payload.max_physical_memory; 5310 } 5311 } else if (vmobj.hasOwnProperty('max_physical_memory')) { 5312 // new payload doesn't have a max_physical, so clamp to vmobj's 5313 if (payload.max_locked_memory > vmobj.max_physical_memory) { 5314 log.warn('max_locked_memory (' + payload.max_locked_memory 5315 + ') > vm.max_physical_memory (' + vmobj.max_physical_memory 5316 + ') clamping to ' + vmobj.max_physical_memory); 5317 payload.max_locked_memory = vmobj.max_physical_memory; 5318 } 5319 } 5320 } 5321 5322 if (payload.hasOwnProperty('max_swap')) { 5323 if (payload.hasOwnProperty('max_physical_memory')) { 5324 if (payload.max_swap < payload.max_physical_memory) { 5325 log.warn('max_swap (' + payload.max_swap 5326 + ') < max_physical_memory (' + payload.max_physical_memory 5327 + ') raising to ' + payload.max_physical_memory); 5328 payload.max_swap = payload.max_physical_memory; 5329 } 5330 } else if (vmobj.hasOwnProperty('max_physical_memory')) { 5331 // new payload doesn't have a max_physical, so raise to vmobj's 5332 if (payload.max_swap < vmobj.max_physical_memory) { 5333 log.warn('max_swap (' + payload.max_swap 5334 + ') < vm.max_physical_memory (' + vmobj.max_physical_memory 5335 + ') raising to ' + vmobj.max_physical_memory); 5336 payload.max_swap = vmobj.max_physical_memory; 5337 } 5338 } 5339 } 5340 } 5341 5342 // generate a new UUID if payload doesn't have one (also ensures that this uuid 5343 // does not already belong to a zone). 5344 function createZoneUUID(payload, log, callback) 5345 { 5346 var uuid; 5347 5348 assert(log, 'no logger passed to createZoneUUID()'); 5349 5350 if (payload.hasOwnProperty('uuid')) { 5351 // Ensure that the uuid is not already used. 5352 getZoneRecords(null, log, function (err, records) { 5353 if (err) { 5354 callback(err); 5355 } else { 5356 if (records.hasOwnProperty(payload.uuid)) { 5357 callback(new Error('vm with UUID ' + payload.uuid 5358 + ' already exists.')); 5359 } else { 5360 callback(null, payload.uuid); 5361 } 5362 } 5363 }); 5364 } else { 5365 log.debug('/usr/bin/uuid -v 4'); 5366 execFile('/usr/bin/uuid', ['-v', '4'], function (err, stdout, stderr) { 5367 if (err) { 5368 callback(err); 5369 return; 5370 } 5371 5372 // chomp trailing spaces and newlines 5373 uuid = stdout.toString().replace(/\s+$/g, ''); 5374 payload.uuid = uuid; 5375 log.info('generated uuid ' + uuid + ' for new VM'); 5376 getZoneRecords(null, log, function (e, records) { 5377 if (e) { 5378 callback(e); 5379 } else { 5380 if (records.hasOwnProperty(payload.uuid)) { 5381 callback(new Error('vm with UUID ' + payload.uuid 5382 + 'already exists.')); 5383 } else { 5384 callback(null, payload.uuid); 5385 } 5386 } 5387 }); 5388 }); 5389 } 5390 } 5391 5392 function applyZoneDefaults(payload, log) 5393 { 5394 var allowed; 5395 var disk; 5396 var disks; 5397 var n; 5398 var nic; 5399 var nics; 5400 var zvol; 5401 5402 assert(log, 'no logger passed to applyZoneDefaults()'); 5403 5404 log.debug('applying zone defaults'); 5405 5406 if (!payload.hasOwnProperty('owner_uuid')) { 5407 // We assume that this all-zero uuid can be treated as 'admin' 5408 payload.owner_uuid = '00000000-0000-0000-0000-000000000000'; 5409 } 5410 5411 if (!payload.hasOwnProperty('autoboot')) { 5412 payload.autoboot = true; 5413 } 5414 5415 if (!payload.hasOwnProperty('brand')) { 5416 payload.brand = 'joyent'; 5417 } 5418 5419 if (!payload.hasOwnProperty('zpool')) { 5420 payload.zpool = 'zones'; 5421 } 5422 5423 if (!payload.hasOwnProperty('dns_domain')) { 5424 payload.dns_domain = 'local'; 5425 } 5426 5427 if (!payload.hasOwnProperty('cpu_shares')) { 5428 payload.cpu_shares = 100; 5429 } else { 5430 if (payload.cpu_shares > 65535) { 5431 log.info('capping cpu_shares at 64k (was: ' 5432 + payload.cpu_shares + ')'); 5433 payload.cpu_shares = 65535; // max is 64K 5434 } 5435 } 5436 5437 if (!payload.hasOwnProperty('zfs_io_priority')) { 5438 payload.zfs_io_priority = 100; 5439 } 5440 5441 if (!payload.hasOwnProperty('max_lwps')) { 5442 payload.max_lwps = 2000; 5443 } 5444 5445 // We need to set the RAM here because we use it as the default for 5446 // the max_physical_memory below. If we've set max_phys and we're not 5447 // KVM, we'll use that instead of ram anyway. 5448 if (!payload.hasOwnProperty('ram')) { 5449 payload.ram = 256; 5450 } 5451 5452 fixPayloadMemory(payload, {}, log); 5453 5454 allowed = BRAND_OPTIONS[payload.brand].allowed_properties; 5455 if (allowed.hasOwnProperty('vcpus') && !payload.hasOwnProperty('vcpus')) { 5456 payload.vcpus = 1; 5457 } 5458 5459 if (BRAND_OPTIONS[payload.brand].features.use_tmpfs 5460 && (!payload.hasOwnProperty('tmpfs') 5461 || (Number(payload.tmpfs) > Number(payload.max_physical_memory)))) { 5462 5463 payload.tmpfs = payload.max_physical_memory; 5464 } 5465 5466 if (!payload.hasOwnProperty('limit_priv')) { 5467 // note: the limit privs are going to be added to the brand and 5468 // shouldn't need to be set here by default when that's done. 5469 if (BRAND_OPTIONS[payload.brand].features.limit_priv) { 5470 payload.limit_priv 5471 = BRAND_OPTIONS[payload.brand].features.limit_priv.join(','); 5472 } else { 5473 payload.limit_priv = 'default'; 5474 } 5475 } 5476 5477 if (!payload.hasOwnProperty('quota')) { 5478 payload.quota = '10'; // in GiB 5479 } 5480 5481 if (!payload.hasOwnProperty('billing_id')) { 5482 payload.billing_id = '00000000-0000-0000-0000-000000000000'; 5483 } 5484 5485 if (payload.hasOwnProperty('add_disks')) { 5486 // update 5487 disks = payload.add_disks; 5488 } else if (payload.hasOwnProperty('disks')) { 5489 disks = payload.disks; 5490 } else { 5491 // no disks at all 5492 disks = []; 5493 } 5494 5495 for (disk in disks) { 5496 if (disks.hasOwnProperty(disk)) { 5497 zvol = disks[disk]; 5498 if (!zvol.hasOwnProperty('model') 5499 && payload.hasOwnProperty('disk_driver')) { 5500 5501 zvol.model = payload.disk_driver; 5502 } 5503 if (!zvol.hasOwnProperty('media')) { 5504 zvol.media = 'disk'; 5505 } 5506 } 5507 } 5508 5509 if (payload.hasOwnProperty('add_nics')) { 5510 // update 5511 nics = payload.add_nics; 5512 } else if (payload.hasOwnProperty('nics')) { 5513 nics = payload.nics; 5514 } else { 5515 // no disks at all 5516 nics = []; 5517 } 5518 5519 for (nic in nics) { 5520 if (nics.hasOwnProperty(nic)) { 5521 n = nics[nic]; 5522 if (!n.hasOwnProperty('model') 5523 && payload.hasOwnProperty('nic_driver')) { 5524 5525 n.model = payload.nic_driver; 5526 } 5527 } 5528 } 5529 } 5530 5531 function validRecordSize(candidate) 5532 { 5533 if (candidate < 512) { 5534 // too low 5535 return (false); 5536 } else if (candidate > 131072) { 5537 // too high 5538 return (false); 5539 } else if ((candidate & (candidate - 1)) !== 0) { 5540 // not a power of 2 5541 return (false); 5542 } 5543 5544 return (true); 5545 } 5546 5547 // This function gets called for both create and update to check that payload 5548 // properties are reasonable. If vmobj is null, create is assumed, otherwise 5549 // update is assumed. 5550 function checkPayloadProperties(payload, vmobj, log, callback) 5551 { 5552 var array_fields = [ 5553 'add_nics', 'update_nics', 'remove_nics', 5554 'add_disks', 'update_disks', 'remove_disks', 5555 'add_filesystems', 'update_filesystems', 'remove_filesystems' 5556 ]; 5557 var changed_nics = []; 5558 var current_ips = []; 5559 var current_macs = []; 5560 var current_primary_ips = []; 5561 var current_vrids = []; 5562 var disk; 5563 var dst; 5564 var field; 5565 var filesys; 5566 var i; 5567 var ips = []; 5568 var is_nic = false; 5569 var live_ok; 5570 var mac; 5571 var macs = []; 5572 var m; 5573 var n; 5574 var nic; 5575 var nics_result = {}; 5576 var nics_result_ordered = []; 5577 var nic_fields = ['add_nics', 'update_nics']; 5578 var only_vrrp_nics = true; 5579 var primary_nics; 5580 var prop; 5581 var props; 5582 var ram; 5583 var route; 5584 var routes_result = {}; 5585 var brand; 5586 var vrids = []; 5587 var zvol; 5588 5589 assert(log, 'no logger passed to checkPayloadProperties()'); 5590 5591 if (vmobj) { 5592 brand = vmobj.brand; 5593 } else if (payload.hasOwnProperty('brand')) { 5594 brand = payload.brand; 5595 } else { 5596 callback(new Error('unable to determine brand for VM')); 5597 } 5598 5599 /* check types of fields that should be arrays */ 5600 for (field in array_fields) { 5601 field = array_fields[field]; 5602 if (payload.hasOwnProperty(field) && ! Array.isArray(payload[field])) { 5603 callback(new Error(field + ' must be an array.')); 5604 return; 5605 } 5606 } 5607 5608 if (!vmobj) { 5609 // This is a CREATE 5610 5611 // These should have already been enforced 5612 if (payload.max_locked_memory > payload.max_physical_memory) { 5613 callback(new Error('max_locked_memory must be <= ' 5614 + 'max_physical_memory')); 5615 return; 5616 } 5617 if (payload.max_swap < payload.max_physical_memory) { 5618 callback(new Error('max_swap must be >= max_physical_memory')); 5619 return; 5620 } 5621 5622 // We used to use zone_path instead of zonepath, so accept that too. 5623 if (payload.hasOwnProperty('zone_path') 5624 && !payload.hasOwnProperty('zonepath')) { 5625 5626 payload.zonepath = payload.zone_path; 5627 delete payload.zone_path; 5628 } 5629 } else { 5630 // This is an UPDATE 5631 5632 // can't update disks of a running VM 5633 if (payload.hasOwnProperty('add_disks') 5634 || payload.hasOwnProperty('remove_disks')) { 5635 5636 if ((vmobj.state !== 'stopped') 5637 || (vmobj.state === 'provisioning' 5638 && vmobj.zone_state !== 'installed')) { 5639 5640 callback(new Error('updates to disks are only allowed when ' 5641 + 'state is "stopped", currently: ' + vmobj.state + ' (' 5642 + vmobj.zone_state + ')')); 5643 return; 5644 } 5645 } 5646 5647 // For update_disks we can update refreservation and compression values 5648 // while running. If there are other parameters to update though we'll 5649 // reject. 5650 if (payload.hasOwnProperty('update_disks')) { 5651 if ((vmobj.state !== 'stopped') 5652 || (vmobj.state === 'provisioning' 5653 && vmobj.zone_state !== 'installed')) { 5654 5655 live_ok = true; 5656 5657 payload.update_disks.forEach(function (d) { 5658 var key; 5659 var keys = Object.keys(d); 5660 5661 while ((keys.length > 0) && live_ok) { 5662 key = keys.pop(); 5663 if ([ 5664 'compression', 5665 'path', 5666 'refreservation' 5667 ].indexOf(key) === -1) { 5668 5669 // this key is not allowed! 5670 live_ok = false; 5671 } 5672 } 5673 }); 5674 5675 if (!live_ok) { 5676 callback(new Error('at least one specified update to disks ' 5677 + 'is only allowed when state is "stopped", currently: ' 5678 + vmobj.state + ' (' + vmobj.zonestate + ')')); 5679 return; 5680 } 5681 } 5682 } 5683 5684 // if there's a min_overhead we ensure values are higher than ram. 5685 if (BRAND_OPTIONS[brand].features.min_memory_overhead) { 5686 if (payload.hasOwnProperty('ram')) { 5687 ram = payload.ram; 5688 } else { 5689 ram = vmobj.ram; 5690 } 5691 5692 // ensure none of these is < ram 5693 if (payload.hasOwnProperty('max_physical_memory') 5694 && payload.max_physical_memory < ram) { 5695 5696 callback(new Error('vm.max_physical_memory (' 5697 + payload.max_physical_memory + ') cannot be lower than' 5698 + ' vm.ram (' + ram + ')')); 5699 return; 5700 } 5701 if (payload.hasOwnProperty('max_locked_memory') 5702 && payload.max_locked_memory < ram) { 5703 5704 callback(new Error('vm.max_locked_memory (' 5705 + payload.max_locked_memory + ') cannot be lower than' 5706 + ' vm.ram (' + ram + ')')); 5707 return; 5708 } 5709 // This should not be allowed anyway because max_swap will be raised 5710 // to match max_physical_memory if you set it lower. 5711 if (payload.hasOwnProperty('max_swap')) { 5712 if (payload.max_swap < ram) { 5713 callback(new Error('vm.max_swap (' 5714 + payload.max_swap + ') cannot be lower than' 5715 + ' vm.ram (' + ram + ')')); 5716 return; 5717 } else if (payload.max_swap < MINIMUM_MAX_SWAP) { 5718 callback(new Error('vm.max_swap (' 5719 + payload.max_swap + ') cannot be lower than ' 5720 + MINIMUM_MAX_SWAP + 'MiB')); 5721 return; 5722 } 5723 } 5724 } 5725 5726 /* 5727 * keep track of current IPs/MACs so we can make sure they're not being 5728 * duplicated. 5729 * 5730 */ 5731 for (nic in vmobj.nics) { 5732 nic = vmobj.nics[nic]; 5733 if (nic.hasOwnProperty('ip') && nic.ip !== 'dhcp') { 5734 current_ips.push(nic.ip); 5735 } 5736 if (nic.hasOwnProperty('mac')) { 5737 current_macs.push(nic.mac); 5738 } 5739 if (nic.hasOwnProperty('vrrp_vrid')) { 5740 current_vrids.push(nic.vrrp_vrid); 5741 } 5742 if (nic.hasOwnProperty('vrrp_primary_ip')) { 5743 current_primary_ips.push(nic.vrrp_primary_ip); 5744 } 5745 5746 if (nic.hasOwnProperty('mac') || nic.hasOwnProperty('vrrp_vrid')) { 5747 mac = nic.hasOwnProperty('mac') ? nic.mac 5748 : vrrpMAC(nic.vrrp_vrid); 5749 if (!nics_result.hasOwnProperty(mac)) { 5750 nics_result[mac] = nic; 5751 nics_result_ordered.push(nic); 5752 } 5753 } 5754 } 5755 5756 // Keep track of route additions / deletions, to make sure that 5757 // we're not setting link-local routes against nics that don't exist 5758 for (route in vmobj.routes) { 5759 routes_result[route] = vmobj.routes[route]; 5760 } 5761 } 5762 5763 if (payload.hasOwnProperty('add_disks')) { 5764 for (disk in payload.add_disks) { 5765 if (payload.add_disks.hasOwnProperty(disk)) { 5766 zvol = payload.add_disks[disk]; 5767 5768 // path is only allowed in 2 cases when adding a disk: 5769 // 5770 // 1) for cdrom devices 5771 // 2) when nocreate is specified 5772 // 5773 if (zvol.hasOwnProperty('path')) { 5774 if (zvol.media !== 'cdrom' && !zvol.nocreate) { 5775 callback(new Error('you cannot specify a path for a ' 5776 + 'disk unless you set nocreate=true')); 5777 return; 5778 } 5779 } 5780 5781 // NOTE: We'll have verified the .zpool argument is a valid 5782 // zpool using VM.validate() if it's set. 5783 5784 if (zvol.hasOwnProperty('block_size') 5785 && !validRecordSize(zvol.block_size)) { 5786 5787 callback(new Error('invalid .block_size(' + zvol.block_size 5788 + '), must be 512-131072 and a power of 2.')); 5789 return; 5790 } 5791 5792 if (zvol.hasOwnProperty('block_size') 5793 && zvol.hasOwnProperty('image_uuid')) { 5794 5795 callback(new Error('setting both .block_size and ' 5796 + '.image_uuid on a volume is invalid')); 5797 } 5798 5799 if (zvol.hasOwnProperty('compression')) { 5800 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) { 5801 callback(new Error('invalid compression setting for ' 5802 + 'disk, must be one of: ' 5803 + VM.COMPRESSION_TYPES.join(', '))); 5804 } 5805 } 5806 5807 if (!zvol.hasOwnProperty('model') 5808 || zvol.model === 'undefined') { 5809 5810 if (vmobj && vmobj.hasOwnProperty('disk_driver')) { 5811 zvol.model = vmobj.disk_driver; 5812 log.debug('set model to ' + zvol.model 5813 + ' from disk_driver'); 5814 } else if (vmobj && vmobj.hasOwnProperty('disks') 5815 && vmobj.disks.length > 0 && vmobj.disks[0].model) { 5816 5817 zvol.model = vmobj.disks[0].model; 5818 log.debug('set model to ' + zvol.model + ' from disk0'); 5819 } else { 5820 callback(new Error('missing .model option for ' 5821 + 'disk: ' + JSON.stringify(zvol))); 5822 return; 5823 } 5824 } else if (VM.DISK_MODELS.indexOf(zvol.model) === -1) { 5825 callback(new Error('"' + zvol.model + '"' 5826 + ' is not a valid disk model. Valid are: ' 5827 + VM.DISK_MODELS.join(','))); 5828 return; 5829 } 5830 } 5831 } 5832 } 5833 5834 if (payload.hasOwnProperty('update_disks')) { 5835 for (disk in payload.update_disks) { 5836 if (payload.update_disks.hasOwnProperty(disk)) { 5837 zvol = payload.update_disks[disk]; 5838 5839 if (zvol.hasOwnProperty('compression')) { 5840 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) { 5841 callback(new Error('invalid compression type for ' 5842 + 'disk, must be one of: ' 5843 + VM.COMPRESSION_TYPES.join(', '))); 5844 } 5845 } 5846 5847 if (zvol.hasOwnProperty('block_size')) { 5848 callback(new Error('cannot change .block_size for a disk ' 5849 + 'after creation')); 5850 return; 5851 } 5852 } 5853 } 5854 } 5855 5856 // If we're receiving, we might not have the filesystem yet 5857 if (!payload.hasOwnProperty('transition') 5858 || payload.transition.transition !== 'receiving') { 5859 5860 for (filesys in payload.filesystems) { 5861 filesys = payload.filesystems[filesys]; 5862 if (!fs.existsSync(filesys.source)) { 5863 callback(new Error('missing requested filesystem: ' 5864 + filesys.source)); 5865 return; 5866 } 5867 } 5868 } 5869 5870 if (payload.hasOwnProperty('default_gateway') 5871 && payload.default_gateway !== '') { 5872 5873 log.warn('DEPRECATED: default_gateway should no longer be used, ' 5874 + 'instead set one NIC primary and use nic.gateway.'); 5875 } 5876 5877 primary_nics = 0; 5878 for (field in nic_fields) { 5879 field = nic_fields[field]; 5880 if (payload.hasOwnProperty(field)) { 5881 for (nic in payload[field]) { 5882 if (payload[field].hasOwnProperty(nic)) { 5883 n = payload[field][nic]; 5884 5885 // MAC will always conflict in update, since that's the key 5886 if (field === 'add_nics' && n.hasOwnProperty('mac')) { 5887 if ((macs.indexOf(n.mac) !== -1) 5888 || current_macs.indexOf(n.mac) !== -1) { 5889 5890 callback(new Error('Cannot add multiple NICs with ' 5891 + 'the same MAC: ' + n.mac)); 5892 return; 5893 } 5894 macs.push(n.mac); 5895 } 5896 5897 if (field === 'add_nics' || field === 'update_nics') { 5898 if (n.hasOwnProperty('primary')) { 5899 if (n.primary !== true) { 5900 callback(new Error('invalid value for NIC\'s ' 5901 + 'primary flag: ' + n.primary + ' (must be' 5902 + ' true)')); 5903 return; 5904 } 5905 primary_nics++; 5906 } 5907 changed_nics.push(n); 5908 } 5909 5910 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') { 5911 if (ips.indexOf(n.ip) !== -1 5912 || current_ips.indexOf(n.ip) !== -1) { 5913 5914 callback(new Error('Cannot add multiple NICs with ' 5915 + 'the same IP: ' + n.ip)); 5916 return; 5917 } 5918 ips.push(n.ip); 5919 } 5920 5921 if (n.hasOwnProperty('vrrp_vrid')) { 5922 if (current_vrids.indexOf(n.vrrp_vrid) !== -1 5923 || vrids.indexOf(n.vrrp_vrid) !== -1) { 5924 callback(new Error('Cannot add multiple NICs with ' 5925 + 'the same VRID: ' + n.vrrp_vrid)); 5926 return; 5927 } 5928 vrids.push(n.vrrp_vrid); 5929 } 5930 5931 if (field === 'add_nics' 5932 && n.hasOwnProperty('vrrp_vrid') 5933 && n.hasOwnProperty('mac')) { 5934 callback( 5935 new Error('Cannot set both mac and vrrp_vrid')); 5936 return; 5937 } 5938 5939 if (n.hasOwnProperty('vrrp_primary_ip')) { 5940 current_primary_ips.push(n.vrrp_primary_ip); 5941 } 5942 5943 if (BRAND_OPTIONS[brand].features.model_required 5944 && field === 'add_nics' 5945 && (!n.hasOwnProperty('model') || !n.model 5946 || n.model === 'undefined' || n.model.length === 0)) { 5947 5948 5949 if (vmobj && vmobj.hasOwnProperty('nic_driver')) { 5950 n.model = vmobj.nic_driver; 5951 log.debug('set model to ' + n.model 5952 + ' from nic_driver'); 5953 } else if (vmobj && vmobj.hasOwnProperty('nics') 5954 && vmobj.nics.length > 0 && vmobj.nics[0].model) { 5955 5956 n.model = vmobj.nics[0].model; 5957 log.debug('set model to ' + n.model + ' from nic0'); 5958 } else { 5959 callback(new Error('missing .model option for NIC: ' 5960 + JSON.stringify(n))); 5961 return; 5962 } 5963 } 5964 5965 if (field === 'add_nics' && n.ip !== 'dhcp' 5966 && (!n.hasOwnProperty('netmask') 5967 || !net.isIPv4(n.netmask))) { 5968 5969 callback(new Error('invalid or missing .netmask option ' 5970 + 'for NIC: ' + JSON.stringify(n))); 5971 return; 5972 } 5973 5974 if ((field === 'add_nics' || field === 'update_nics') 5975 && n.hasOwnProperty('ip') && n.ip !== 'dhcp' 5976 && !net.isIPv4(n.ip)) { 5977 5978 callback(new Error('invalid IP for NIC: ' 5979 + JSON.stringify(n))); 5980 return; 5981 } 5982 5983 if (field === 'add_nics' && (!n.hasOwnProperty('nic_tag') 5984 || !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/))) { 5985 5986 callback(new Error('invalid or missing .nic_tag option ' 5987 + 'for NIC: ' + JSON.stringify(n))); 5988 return; 5989 } 5990 5991 if (field === 'update_nics' && n.hasOwnProperty('model') 5992 && (!n.model || n.model === 'undefined' 5993 || n.model.length === 0)) { 5994 5995 callback(new Error('invalid .model option for NIC: ' 5996 + JSON.stringify(n))); 5997 return; 5998 } 5999 6000 if (field === 'update_nics' && n.hasOwnProperty('netmask') 6001 && (!n.netmask || !net.isIPv4(n.netmask))) { 6002 6003 callback(new Error('invalid .netmask option for NIC: ' 6004 + JSON.stringify(n))); 6005 return; 6006 } 6007 6008 if (field === 'update_nics' && n.hasOwnProperty('nic_tag') 6009 && !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/)) { 6010 6011 callback(new Error('invalid .nic_tag option for NIC: ' 6012 + JSON.stringify(n))); 6013 return; 6014 } 6015 6016 if (n.hasOwnProperty('mac') 6017 || n.hasOwnProperty('vrrp_vrid')) { 6018 mac = n.hasOwnProperty('mac') ? n.mac 6019 : vrrpMAC(n.vrrp_vrid); 6020 if (nics_result.hasOwnProperty(mac)) { 6021 var p; 6022 for (p in n) { 6023 nics_result[mac][p] = n[p]; 6024 } 6025 6026 nics_result_ordered.forEach(function (on) { 6027 if (on.hasOwnProperty('mac') && on.mac == mac) { 6028 for (p in n) { 6029 on[p] = n[p]; 6030 } 6031 } 6032 }); 6033 } else { 6034 nics_result[mac] = n; 6035 nics_result_ordered.push(n); 6036 } 6037 } 6038 6039 if ((field === 'add_nics' || field === 'update_nics') 6040 && n.hasOwnProperty('allowed_ips')) { 6041 try { 6042 validateIPlist(n.allowed_ips); 6043 } catch (ipListErr) { 6044 callback(ipListErr); 6045 return; 6046 } 6047 } 6048 } 6049 } 6050 } 6051 } 6052 6053 if (payload.hasOwnProperty('remove_nics')) { 6054 for (m in payload.remove_nics) { 6055 m = payload.remove_nics[m]; 6056 n = nics_result[m]; 6057 if (!n) { 6058 continue; 6059 } 6060 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') { 6061 i = ips.indexOf(n.ip); 6062 if (i !== -1) { 6063 ips.splice(i, 1); 6064 } 6065 i = current_ips.indexOf(n.ip); 6066 if (i !== -1) { 6067 current_ips.splice(i, 1); 6068 } 6069 } 6070 delete nics_result[m]; 6071 6072 for (i in nics_result_ordered) { 6073 n = nics_result_ordered[i]; 6074 if (n.hasOwnProperty('mac') && n.mac == m) { 6075 nics_result_ordered.splice(i, 1); 6076 break; 6077 } 6078 } 6079 } 6080 } 6081 6082 // nics_result now has the state of the nics after the update - now check 6083 // properties that depend on each other or on other nics 6084 for (n in nics_result) { 6085 n = nics_result[n]; 6086 if (n.hasOwnProperty('vrrp_vrid')) { 6087 if (n.hasOwnProperty('ip') 6088 && current_primary_ips.indexOf(n.ip) !== -1) { 6089 callback( 6090 new Error( 6091 'Cannot set vrrp_primary_ip to the IP of a VRRP nic')); 6092 return; 6093 } 6094 6095 if (!n.hasOwnProperty('vrrp_primary_ip')) { 6096 callback(new Error( 6097 'vrrp_vrid set but not vrrp_primary_ip')); 6098 return; 6099 } 6100 } else { 6101 only_vrrp_nics = false; 6102 } 6103 } 6104 6105 if (only_vrrp_nics && Object.keys(nics_result).length !== 0) { 6106 callback(new Error('VM cannot contain only VRRP nics')); 6107 return; 6108 } 6109 6110 for (i in current_primary_ips) { 6111 i = current_primary_ips[i]; 6112 if ((current_ips.indexOf(i) === -1) 6113 && (ips.indexOf(i) === -1)) { 6114 callback(new Error( 6115 'vrrp_primary_ip must belong to the same VM')); 6116 return; 6117 } 6118 } 6119 6120 // Since we always need a primary nic, don't allow a value other than true 6121 // for primary flag. Also ensure we're not trying to set primary for more 6122 // than one nic. 6123 if (primary_nics > 1) { 6124 callback(new Error('payload specifies more than 1 primary NIC')); 6125 return; 6126 } 6127 6128 if (payload.hasOwnProperty('vga') 6129 && VM.VGA_TYPES.indexOf(payload.vga) === -1) { 6130 6131 callback(new Error('Invalid VGA type: "' + payload.vga 6132 + '", supported types are: ' + VM.VGA_TYPES.join(','))); 6133 return; 6134 } 6135 6136 function validLocalRoute(r) { 6137 var nicIdx = r.match(/nics\[(\d+)\]/); 6138 if (!nicIdx) { 6139 is_nic = false; 6140 return false; 6141 } 6142 is_nic = true; 6143 6144 if (nics_result_ordered.length === 0) { 6145 return false; 6146 } 6147 6148 nicIdx = Number(nicIdx[1]); 6149 if (!nics_result_ordered[nicIdx] 6150 || !nics_result_ordered[nicIdx].hasOwnProperty('ip') 6151 || nics_result_ordered[nicIdx].ip === 'dhcp') { 6152 return false; 6153 } 6154 6155 return true; 6156 } 6157 6158 props = [ 'routes', 'set_routes' ]; 6159 for (prop in props) { 6160 prop = props[prop]; 6161 if (payload.hasOwnProperty(prop)) { 6162 for (dst in payload[prop]) { 6163 var src = payload[prop][dst]; 6164 6165 if (!net.isIPv4(dst) && !isCIDR(dst)) { 6166 callback(new Error('Invalid route destination: "' + dst 6167 + '" (must be IP address or CIDR)')); 6168 return; 6169 } 6170 6171 if (!net.isIPv4(src) && !validLocalRoute(src)) { 6172 callback(new Error( 6173 is_nic ? 'Route gateway: "' + src 6174 + '" refers to non-existent or DHCP nic' 6175 : 'Invalid route gateway: "' + src 6176 + '" (must be IP address or nic)')); 6177 return; 6178 } 6179 6180 routes_result[dst] = src; 6181 } 6182 } 6183 } 6184 6185 if (payload.hasOwnProperty('remove_routes')) { 6186 for (dst in payload.remove_routes) { 6187 dst = payload.remove_routes[dst]; 6188 delete routes_result[dst]; 6189 } 6190 } 6191 6192 // Now that we've applied all updates to routes, make sure that all 6193 // link-local routes refer to a nic that still exists 6194 for (dst in routes_result) { 6195 if (!net.isIPv4(routes_result[dst]) 6196 && !validLocalRoute(routes_result[dst])) { 6197 callback(new Error('Route gateway: "' + routes_result[dst] 6198 + '" refers to non-existent or DHCP nic')); 6199 return; 6200 } 6201 } 6202 6203 // Ensure password is not too long 6204 if (payload.hasOwnProperty('vnc_password') 6205 && payload.vnc_password.length > 8) { 6206 6207 callback(new Error('VNC password is too long, maximum length is 8 ' 6208 + 'characters.')); 6209 return; 6210 } 6211 6212 props = ['zfs_root_recsize', 'zfs_data_recsize']; 6213 for (prop in props) { 6214 prop = props[prop]; 6215 if (payload.hasOwnProperty(prop)) { 6216 if (payload[prop] === 0 || payload[prop] === '') { 6217 // this is the default, so set it back to that. 6218 payload[prop] = 131072; 6219 } else if (!validRecordSize(payload[prop])) { 6220 callback(new Error('invalid ' + prop + ' (' + payload[prop] 6221 + '), must be 512-131072 and a power of 2. ' 6222 + '(0 to disable)')); 6223 return; 6224 } 6225 } 6226 } 6227 props = ['zfs_root_compression', 'zfs_data_compression']; 6228 for (prop in props) { 6229 prop = props[prop]; 6230 6231 if (payload.hasOwnProperty(prop)) { 6232 if (VM.COMPRESSION_TYPES.indexOf(payload[prop]) === -1) { 6233 callback(new Error('invalid compression type for ' 6234 + payload[prop] + ', must be one of: ' 6235 + VM.COMPRESSION_TYPES.join(', '))); 6236 } 6237 } 6238 } 6239 6240 // Ensure MACs and IPs are not already used on this vm 6241 // NOTE: can't check other nodes yet. 6242 6243 async.series([ 6244 function (cb) { 6245 lookupConflicts(macs, ips, vrids, log, function (error, conflict) { 6246 if (error) { 6247 cb(error); 6248 } else { 6249 if (conflict) { 6250 cb(new Error('Conflict detected with another ' 6251 + 'vm, please check the MAC, IP, and VRID')); 6252 } else { 6253 log.debug('no conflicts'); 6254 cb(); 6255 } 6256 } 6257 }); 6258 }, function (cb) { 6259 lookupInvalidNicTags(changed_nics, log, function (e) { 6260 if (e) { 6261 cb(e); 6262 } else { 6263 cb(); 6264 } 6265 }); 6266 }, function (cb) { 6267 // We only allow adding firewall rules on create 6268 if (vmobj) { 6269 log.debug('update: not validating firewall data'); 6270 cb(); 6271 return; 6272 } 6273 6274 if (!payload.hasOwnProperty('firewall')) { 6275 log.debug('no firewall data in payload: not validating'); 6276 cb(); 6277 return; 6278 } 6279 validateFirewall(payload, log, cb); 6280 } 6281 ], function (err) { 6282 log.trace('leaving checkPayloadProperties()'); 6283 callback(err); 6284 }); 6285 } 6286 6287 function createDelegatedDataset(payload, log, callback) 6288 { 6289 var args; 6290 var ds; 6291 var zcfg = ''; 6292 6293 assert(log, 'no logger passed to createDelegatedDataset()'); 6294 6295 if (payload.delegate_dataset) { 6296 log.info('creating delegated dataset.'); 6297 if (!payload.hasOwnProperty('zfs_filesystem')) { 6298 callback(new Error('payload missing zfs_filesystem')); 6299 return; 6300 } 6301 ds = path.join(payload.zfs_filesystem, '/data'); 6302 6303 args = ['create']; 6304 if (payload.hasOwnProperty('zfs_data_compression')) { 6305 args.push('-o', 'compression=' + payload.zfs_data_compression); 6306 } 6307 if (payload.hasOwnProperty('zfs_data_recsize')) { 6308 args.push('-o', 'recsize=' + payload.zfs_data_recsize); 6309 } 6310 args.push(ds); 6311 6312 zfs(args, log, function (err) { 6313 if (err) { 6314 callback(err); 6315 return; 6316 } 6317 6318 zcfg = zcfg + 'add dataset; set name=' + ds + '; end\n'; 6319 zonecfg(['-u', payload.uuid, zcfg], log, function (e, fds) { 6320 if (e) { 6321 log.error({'err': e, stdout: fds.stdout, 6322 stderr: fds.stderr}, 'unable to add delegated dataset ' 6323 + ds + ' to ' + payload.uuid); 6324 callback(e); 6325 } else { 6326 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 6327 'added delegated dataset ' + ds); 6328 callback(); 6329 } 6330 }); 6331 }); 6332 } else { 6333 callback(); 6334 } 6335 } 6336 6337 function buildAddRemoveList(vmobj, payload, type, key, updatable) 6338 { 6339 var add = []; 6340 var add_key; 6341 var field; 6342 var newobj; 6343 var oldobj; 6344 var plural = type + 's'; 6345 var remove = []; 6346 var remove_key; 6347 var update_key; 6348 6349 // initialize some plurals 6350 add_key = 'add_' + plural; 6351 remove_key = 'remove_' + plural; 6352 update_key = 'update_' + plural; 6353 6354 // There's no way to update properties on a disk or nic with zonecfg 6355 // currently. Yes, really. So any disks/nics that should be updated, we 6356 // remove then add with the new properties. 6357 if (payload.hasOwnProperty(update_key)) { 6358 for (newobj in payload[update_key]) { 6359 newobj = payload[update_key][newobj]; 6360 for (oldobj in vmobj[plural]) { 6361 oldobj = vmobj[plural][oldobj]; 6362 6363 if (oldobj[key] === newobj[key]) { 6364 // This is the one to update: remove and add. 6365 remove.push(oldobj[key]); 6366 6367 // only some fields make sense to update. 6368 for (field in updatable) { 6369 field = updatable[field]; 6370 if (newobj.hasOwnProperty(field)) { 6371 oldobj[field] = newobj[field]; 6372 } 6373 } 6374 6375 add.push(oldobj); 6376 } 6377 } 6378 } 6379 } 6380 6381 if (payload.hasOwnProperty(remove_key)) { 6382 for (newobj in payload[remove_key]) { 6383 newobj = payload[remove_key][newobj]; 6384 remove.push(newobj); 6385 } 6386 } 6387 6388 if (payload.hasOwnProperty(add_key)) { 6389 for (newobj in payload[add_key]) { 6390 newobj = payload[add_key][newobj]; 6391 add.push(newobj); 6392 } 6393 } 6394 6395 return ({'add': add, 'remove': remove}); 6396 } 6397 6398 function buildDiskZonecfg(vmobj, payload) 6399 { 6400 var add = []; 6401 var disk; 6402 var lists; 6403 var remove = []; 6404 var zcfg = ''; 6405 6406 lists = buildAddRemoveList(vmobj, payload, 'disk', 'path', 6407 UPDATABLE_DISK_PROPS); 6408 remove = lists.remove; 6409 add = lists.add; 6410 6411 // remove is a list of disk paths, add a remove for each now. 6412 for (disk in remove) { 6413 disk = remove[disk]; 6414 zcfg = zcfg + 'remove -F device match=' + disk + '\n'; 6415 } 6416 6417 for (disk in add) { 6418 disk = add[disk]; 6419 6420 zcfg = zcfg + 'add device\n' 6421 + 'set match=' + disk.path + '\n' 6422 + 'add property (name=boot, value="' 6423 + (disk.boot ? 'true' : 'false') + '")\n' 6424 + 'add property (name=model, value="' + disk.model + '")\n'; 6425 6426 if (disk.hasOwnProperty('media')) { 6427 zcfg = zcfg 6428 + 'add property (name=media, value="' 6429 + disk.media + '")\n'; 6430 } 6431 6432 if (disk.hasOwnProperty('image_size')) { 6433 zcfg = zcfg 6434 + 'add property (name=image-size, value="' 6435 + disk.image_size + '")\n'; 6436 } else if (disk.hasOwnProperty('size')) { 6437 zcfg = zcfg + 'add property (name=size, value="' 6438 + disk.size + '")\n'; 6439 } 6440 6441 if (disk.hasOwnProperty('image_uuid')) { 6442 zcfg = zcfg 6443 + 'add property (name=image-uuid, value="' 6444 + disk.image_uuid + '")\n'; 6445 } 6446 6447 if (disk.hasOwnProperty('image_name')) { 6448 zcfg = zcfg + 'add property (name=image-name, value="' 6449 + disk.image_name + '")\n'; 6450 } 6451 6452 zcfg = zcfg + 'end\n'; 6453 } 6454 6455 return zcfg; 6456 } 6457 6458 function buildNicZonecfg(vmobj, payload) 6459 { 6460 var add; 6461 var lists; 6462 var matches; 6463 var n; 6464 var new_primary; 6465 var nic; 6466 var nic_idx = 0; 6467 var remove; 6468 var updated_primary; 6469 var used_nic_indexes = []; 6470 var zcfg = ''; 6471 6472 if (vmobj.hasOwnProperty('nics')) { 6473 // check whether we're adding or updating to set the primary flag. If we 6474 // are also find the existing NIC with the primary flag. If that's not 6475 // being removed, update it to remove the primary flag. 6476 if (payload.hasOwnProperty('add_nics')) { 6477 for (nic in payload.add_nics) { 6478 nic = payload.add_nics[nic]; 6479 if (nic.hasOwnProperty('primary')) { 6480 new_primary = nic.mac; 6481 } 6482 } 6483 } 6484 if (payload.hasOwnProperty('update_nics')) { 6485 for (nic in payload.update_nics) { 6486 nic = payload.update_nics[nic]; 6487 if (nic.hasOwnProperty('primary')) { 6488 new_primary = nic.mac; 6489 } 6490 } 6491 } 6492 if (new_primary) { 6493 // find old primary 6494 for (nic in vmobj.nics) { 6495 nic = vmobj.nics[nic]; 6496 if (nic.hasOwnProperty('primary') && nic.mac !== new_primary) { 6497 // we have a new primary, so un-primary the old. 6498 if (payload.hasOwnProperty('remove_nics') 6499 && payload.remove_nics.indexOf(nic.mac) !== -1) { 6500 6501 // we're removing the old primary so: done. 6502 break; 6503 } else if (payload.hasOwnProperty('update_nics')) { 6504 updated_primary = false; 6505 for (n in payload.update_nics) { 6506 n = payload.update_nics[n]; 6507 if (n.mac === nic.mac) { 6508 n.primary = false; 6509 updated_primary = true; 6510 } 6511 } 6512 if (!updated_primary) { 6513 payload.update_nics.push({'mac': nic.mac, 6514 'primary': false}); 6515 } 6516 } else { 6517 // just add a new update to unset the 6518 payload.update_nics = 6519 [ {'mac': nic.mac, 'primary': false} ]; 6520 } 6521 } 6522 } 6523 } 6524 } 6525 6526 lists = buildAddRemoveList(vmobj, payload, 'nic', 'mac', 6527 UPDATABLE_NIC_PROPS); 6528 remove = lists.remove; 6529 add = lists.add; 6530 6531 // create a list of used indexes so we can find the free ones 6532 if (vmobj.hasOwnProperty('nics')) { 6533 for (n in vmobj.nics) { 6534 if (vmobj.nics[n].hasOwnProperty('interface')) { 6535 matches = vmobj.nics[n].interface.match(/^net(\d+)$/); 6536 if (matches) { 6537 used_nic_indexes.push(Number(matches[1])); 6538 } 6539 } 6540 } 6541 } 6542 6543 // assign next available interface for nics without one 6544 for (nic in add) { 6545 nic = add[nic]; 6546 if (!nic.hasOwnProperty('interface')) { 6547 while (used_nic_indexes.indexOf(nic_idx) !== -1) { 6548 nic_idx++; 6549 } 6550 nic.interface = 'net' + nic_idx; 6551 used_nic_indexes.push(Number(nic_idx)); 6552 } 6553 6554 // Changing the VRID changes the MAC address too, since the VRID is 6555 // encoded in the MAC. This can't be done until after 6556 // buildAddRemoveList above, since mac is used as the key to figure 6557 // out which nic is which 6558 if (nic.hasOwnProperty('vrrp_vrid')) { 6559 nic.mac = vrrpMAC(nic.vrrp_vrid); 6560 } 6561 } 6562 6563 // remove is a list of nic macs, add a remove for each now. 6564 for (nic in remove) { 6565 nic = remove[nic]; 6566 zcfg = zcfg + 'remove net mac-addr=' + ruinMac(nic) + '\n'; 6567 } 6568 6569 // properties that don't require any validation - add them if they're 6570 // present: 6571 var nicProperties = ['ip', 'netmask', 'network_uuid', 'model', 6572 'dhcp_server', 'allow_dhcp_spoofing', 'blocked_outgoing_ports', 6573 'allow_ip_spoofing', 'allow_mac_spoofing', 'allow_restricted_traffic', 6574 'allow_unfiltered_promisc', 'vrrp_vrid', 'vrrp_primary_ip']; 6575 6576 for (nic in add) { 6577 nic = add[nic]; 6578 6579 zcfg = zcfg 6580 + 'add net\n' 6581 + 'set physical=' + nic.interface + '\n' 6582 + 'set mac-addr=' + ruinMac(nic.mac) + '\n'; 6583 6584 if (nic.hasOwnProperty('nic_tag')) { 6585 zcfg = zcfg + 'set global-nic=' + nic.nic_tag + '\n'; 6586 } 6587 6588 if (nic.hasOwnProperty('gateway') && nic.gateway.length > 0) { 6589 zcfg = zcfg + 'add property (name=gateway, value="' 6590 + nic.gateway + '")\n'; 6591 } 6592 6593 if (nic.hasOwnProperty('primary') && nic.primary) { 6594 zcfg = zcfg + 'add property (name=primary, value="true")\n'; 6595 } 6596 6597 if (nic.hasOwnProperty('vlan_id') && (nic.vlan_id !== '0')) { 6598 zcfg = zcfg + 'set vlan-id=' + nic.vlan_id + '\n'; 6599 } 6600 6601 if (nic.hasOwnProperty('allowed_ips')) { 6602 zcfg = zcfg 6603 + 'add property (name=allowed_ips, value="' 6604 + nic.allowed_ips.join(',') + '")\n'; 6605 } 6606 6607 for (var prop in nicProperties) { 6608 prop = nicProperties[prop]; 6609 if (nic.hasOwnProperty(prop)) { 6610 zcfg = zcfg + 'add property (name=' + prop + ', value="' 6611 + nic[prop] + '")\n'; 6612 } 6613 } 6614 6615 zcfg = zcfg + 'end\n'; 6616 } 6617 6618 return zcfg; 6619 } 6620 6621 function buildFilesystemZonecfg(vmobj, payload) 6622 { 6623 var add = []; 6624 var filesystem; 6625 var lists; 6626 var opt; 6627 var remove = []; 6628 var zcfg = ''; 6629 6630 lists = buildAddRemoveList(vmobj, payload, 'filesystem', 'target', []); 6631 remove = lists.remove; 6632 add = lists.add; 6633 6634 // remove is a list of disk paths, add a remove for each now. 6635 for (filesystem in remove) { 6636 filesystem = remove[filesystem]; 6637 zcfg = zcfg + 'remove fs match=' + filesystem + '\n'; 6638 } 6639 6640 for (filesystem in add) { 6641 filesystem = add[filesystem]; 6642 6643 zcfg = zcfg + 'add fs\n' + 'set dir=' + filesystem.target + '\n' 6644 + 'set special=' + filesystem.source + '\n' + 'set type=' 6645 + filesystem.type + '\n'; 6646 if (filesystem.hasOwnProperty('raw')) { 6647 zcfg = zcfg + 'set raw=' + filesystem.raw + '\n'; 6648 } 6649 if (filesystem.hasOwnProperty('options')) { 6650 for (opt in filesystem.options) { 6651 opt = filesystem.options[opt]; 6652 zcfg = zcfg + 'add options "' + opt + '"\n'; 6653 } 6654 } 6655 zcfg = zcfg + 'end\n'; 6656 } 6657 6658 return zcfg; 6659 } 6660 6661 function buildZonecfgUpdate(vmobj, payload, log) 6662 { 6663 var brand; 6664 var tmp; 6665 var zcfg = ''; 6666 6667 assert(log, 'no logger passed to buildZonecfgUpdate()'); 6668 6669 log.debug({vmobj: vmobj, payload: payload}, 6670 'parameters to buildZonecfgUpdate()'); 6671 6672 if (vmobj && vmobj.hasOwnProperty('brand')) { 6673 brand = vmobj.brand; 6674 } else { 6675 brand = payload.brand; 6676 } 6677 6678 // Global properties can just be set, no need to clear anything first. 6679 if (payload.hasOwnProperty('cpu_shares')) { 6680 zcfg = zcfg + 'set cpu-shares=' + payload.cpu_shares.toString() + '\n'; 6681 } 6682 if (payload.hasOwnProperty('zfs_io_priority')) { 6683 zcfg = zcfg + 'set zfs-io-priority=' 6684 + payload.zfs_io_priority.toString() + '\n'; 6685 } 6686 if (payload.hasOwnProperty('max_lwps')) { 6687 zcfg = zcfg + 'set max-lwps=' + payload.max_lwps.toString() + '\n'; 6688 } 6689 if (payload.hasOwnProperty('limit_priv')) { 6690 zcfg = zcfg + 'set limitpriv="' + payload.limit_priv + '"\n'; 6691 } 6692 6693 if (!BRAND_OPTIONS[brand].features.use_vm_autoboot 6694 && payload.hasOwnProperty('autoboot')) { 6695 6696 // kvm autoboot is managed by the vm-autoboot attr instead 6697 zcfg = zcfg + 'set autoboot=' + payload.autoboot.toString() + '\n'; 6698 } 6699 6700 // Capped Memory properties are special 6701 if (payload.hasOwnProperty('max_physical_memory') 6702 || payload.hasOwnProperty('max_locked_memory') 6703 || payload.hasOwnProperty('max_swap')) { 6704 6705 // Capped memory parameters need either an add or select first. 6706 if (vmobj.hasOwnProperty('max_physical_memory') 6707 || vmobj.hasOwnProperty('max_locked_memory') 6708 || vmobj.hasOwnProperty('max_swap')) { 6709 6710 // there's already a capped-memory section, use that. 6711 zcfg = zcfg + 'select capped-memory; '; 6712 } else { 6713 zcfg = zcfg + 'add capped-memory; '; 6714 } 6715 6716 if (payload.hasOwnProperty('max_physical_memory')) { 6717 zcfg = zcfg + 'set physical=' 6718 + payload.max_physical_memory.toString() + 'm; '; 6719 } 6720 if (payload.hasOwnProperty('max_locked_memory')) { 6721 zcfg = zcfg + 'set locked=' 6722 + payload.max_locked_memory.toString() + 'm; '; 6723 } 6724 if (payload.hasOwnProperty('max_swap')) { 6725 zcfg = zcfg + 'set swap=' 6726 + payload.max_swap.toString() + 'm; '; 6727 } 6728 6729 zcfg = zcfg + 'end\n'; 6730 } 6731 6732 // Capped CPU is special 6733 if (payload.hasOwnProperty('cpu_cap')) { 6734 if (vmobj.hasOwnProperty('cpu_cap')) { 6735 zcfg = zcfg + 'select capped-cpu; '; 6736 } else { 6737 zcfg = zcfg + 'add capped-cpu; '; 6738 } 6739 6740 zcfg = zcfg + 'set ncpus=' 6741 + (Number(payload.cpu_cap) * 0.01).toString() + '; end\n'; 6742 } 6743 6744 // set to empty string so property is removed when not true or when not 6745 // false if that's the default for the property. 6746 if (payload.hasOwnProperty('do_not_inventory')) { 6747 if (payload.do_not_inventory !== true) { 6748 // removing sets false as that's the default. 6749 payload.do_not_inventory = ''; 6750 } 6751 } 6752 6753 if (payload.hasOwnProperty('archive_on_delete')) { 6754 if (payload.archive_on_delete !== true) { 6755 // removing sets false as that's the default. 6756 payload.archive_on_delete = ''; 6757 } 6758 } 6759 6760 if (payload.hasOwnProperty('firewall_enabled')) { 6761 if (payload.firewall_enabled !== true) { 6762 // removing sets false as that's the default. 6763 payload.firewall_enabled = ''; 6764 } 6765 } 6766 6767 if (payload.hasOwnProperty('restart_init')) { 6768 if (payload.restart_init === true) { 6769 // removing sets true as that's the default. 6770 payload.restart_init = ''; 6771 } 6772 } 6773 6774 // Attributes 6775 function setAttr(attr, attr_name, value) { 6776 if (!value) { 6777 value = payload[attr_name]; 6778 } 6779 6780 if (payload.hasOwnProperty(attr_name)) { 6781 if ((typeof (value) !== 'boolean') 6782 && (!value || trim(value.toString()) === '')) { 6783 6784 // empty values we either remove or ignore. 6785 if (vmobj.hasOwnProperty(attr_name)) { 6786 zcfg = zcfg + 'remove attr name=' + attr + ';'; 6787 // else do nothing, we don't add empty values. 6788 } 6789 } else { 6790 if (attr_name === 'resolvers' 6791 && vmobj.hasOwnProperty('resolvers') 6792 && vmobj.resolvers.length === 0) { 6793 6794 // special case for resolvers: we always have 'resolvers' 6795 // in the object, but if it's empty we don't have it in the 6796 // zonecfg. Add instead of the usual update. 6797 zcfg = zcfg + 'add attr; set name="' + attr + '"; ' 6798 + 'set type=string; '; 6799 } else if (vmobj.hasOwnProperty(attr_name)) { 6800 zcfg = zcfg + 'select attr name=' + attr + '; '; 6801 } else { 6802 zcfg = zcfg + 'add attr; set name="' + attr + '"; ' 6803 + 'set type=string; '; 6804 } 6805 zcfg = zcfg + 'set value="' + value.toString() + '"; end\n'; 6806 } 6807 } 6808 } 6809 setAttr('billing-id', 'billing_id'); 6810 setAttr('owner-uuid', 'owner_uuid'); 6811 setAttr('package-name', 'package_name'); 6812 setAttr('package-version', 'package_version'); 6813 setAttr('tmpfs', 'tmpfs'); 6814 setAttr('hostname', 'hostname'); 6815 setAttr('dns-domain', 'dns_domain'); 6816 setAttr('default-gateway', 'default_gateway'); 6817 setAttr('do-not-inventory', 'do_not_inventory'); 6818 setAttr('archive-on-delete', 'archive_on_delete'); 6819 setAttr('firewall-enabled', 'firewall_enabled'); 6820 setAttr('restart-init', 'restart_init'); 6821 setAttr('init-name', 'init_name'); 6822 setAttr('disk-driver', 'disk_driver'); 6823 setAttr('nic-driver', 'nic_driver'); 6824 6825 if (payload.hasOwnProperty('resolvers')) { 6826 setAttr('resolvers', 'resolvers', payload.resolvers.join(',')); 6827 } 6828 if (payload.hasOwnProperty('alias')) { 6829 tmp = ''; 6830 if (payload.alias) { 6831 tmp = new Buffer(payload.alias).toString('base64'); 6832 } 6833 setAttr('alias', 'alias', tmp); 6834 } 6835 6836 if (BRAND_OPTIONS[brand].features.use_vm_autoboot) { 6837 setAttr('vm-autoboot', 'autoboot'); 6838 } 6839 6840 // XXX Used on KVM but can be passed in for 'OS' too. We only setAttr on KVM 6841 if (BRAND_OPTIONS[brand].features.type === 'KVM') { 6842 setAttr('ram', 'ram'); 6843 } 6844 6845 // NOTE: Thanks to normalizePayload() we'll only have these when relevant 6846 setAttr('vcpus', 'vcpus'); 6847 setAttr('boot', 'boot'); 6848 setAttr('cpu-type', 'cpu_type'); 6849 setAttr('vga', 'vga'); 6850 setAttr('vnc-port', 'vnc_port'); 6851 setAttr('spice-port', 'spice_port'); 6852 setAttr('virtio-txtimer', 'virtio_txtimer'); 6853 setAttr('virtio-txburst', 'virtio_txburst'); 6854 6855 // We use base64 here for these next five options: 6856 // 6857 // vnc_password 6858 // spice_password 6859 // spice_opts 6860 // qemu_opts 6861 // qemu_extra_opts 6862 // 6863 // since these can contain characters zonecfg doesn't like. 6864 // 6865 if (payload.hasOwnProperty('vnc_password')) { 6866 if (payload.vnc_password === '' 6867 && (vmobj.hasOwnProperty('vnc_password') 6868 && vmobj.vnc_password !== '')) { 6869 6870 log.warn('Warning: VNC password was removed for VM ' 6871 + vmobj.uuid + ' but VM needs to be restarted for change to' 6872 + 'take effect.'); 6873 } 6874 if (payload.vnc_password.length > 0 6875 && !vmobj.hasOwnProperty('vnc_password')) { 6876 6877 log.warn('Warning: VNC password was added to VM ' 6878 + vmobj.uuid + ' but VM needs to be restarted for change to' 6879 + 'take effect.'); 6880 } 6881 6882 setAttr('vnc-password', 'vnc_password', 6883 new Buffer(payload.vnc_password).toString('base64')); 6884 } 6885 if (payload.hasOwnProperty('spice_password')) { 6886 if (payload.spice_password === '' 6887 && (vmobj.hasOwnProperty('spice_password') 6888 && vmobj.spice_password !== '')) { 6889 6890 log.warn('Warning: SPICE password was removed for VM ' 6891 + vmobj.uuid + ' but VM needs to be restarted for change to' 6892 + 'take effect.'); 6893 } 6894 if (payload.spice_password.length > 0 6895 && !vmobj.hasOwnProperty('spice_password')) { 6896 6897 log.warn('Warning: SPICE password was added to VM ' 6898 + vmobj.uuid + ' but VM needs to be restarted for change to' 6899 + 'take effect.'); 6900 } 6901 6902 setAttr('spice-password', 'spice_password', 6903 new Buffer(payload.spice_password).toString('base64')); 6904 } 6905 if (payload.hasOwnProperty('spice_opts')) { 6906 setAttr('spice-opts', 'spice_opts', 6907 new Buffer(payload.spice_opts).toString('base64')); 6908 } 6909 if (payload.hasOwnProperty('qemu_opts')) { 6910 setAttr('qemu-opts', 'qemu_opts', 6911 new Buffer(payload.qemu_opts).toString('base64')); 6912 } 6913 if (payload.hasOwnProperty('qemu_extra_opts')) { 6914 setAttr('qemu-extra-opts', 'qemu_extra_opts', 6915 new Buffer(payload.qemu_extra_opts).toString('base64')); 6916 } 6917 6918 // Handle disks 6919 if (payload.hasOwnProperty('disks') 6920 || payload.hasOwnProperty('add_disks') 6921 || payload.hasOwnProperty('update_disks') 6922 || payload.hasOwnProperty('remove_disks')) { 6923 6924 zcfg = zcfg + buildDiskZonecfg(vmobj, payload); 6925 } 6926 6927 if (payload.hasOwnProperty('fs_allowed')) { 6928 if (payload.fs_allowed === '') { 6929 zcfg = zcfg + 'clear fs-allowed\n'; 6930 } else { 6931 zcfg = zcfg + 'set fs-allowed="' + payload.fs_allowed + '"\n'; 6932 } 6933 } 6934 6935 if (payload.hasOwnProperty('filesystems') 6936 || payload.hasOwnProperty('add_filesystems') 6937 || payload.hasOwnProperty('update_filesystems') 6938 || payload.hasOwnProperty('add_filesystems')) { 6939 6940 zcfg = zcfg + buildFilesystemZonecfg(vmobj, payload); 6941 } 6942 6943 zcfg = zcfg + buildNicZonecfg(vmobj, payload); 6944 6945 return zcfg; 6946 } 6947 6948 // Checks that QMP is responding to query-status and if so passes the boolean 6949 // value of the hwsetup parameter to the callback. 6950 // 6951 // vmobj must have: 6952 // 6953 // zonepath 6954 // 6955 function checkHWSetup(vmobj, log, callback) 6956 { 6957 var q; 6958 var socket; 6959 6960 assert(log, 'no logger passed to checkHWSetup()'); 6961 6962 q = new Qmp(log); 6963 socket = vmobj.zonepath + '/root/tmp/vm.qmp'; 6964 6965 q.connect(socket, function (error) { 6966 if (error) { 6967 log.error(error, 'q.connect(): Error: ' + error.message); 6968 callback(error); 6969 return; 6970 } 6971 q.command('query-status', null, function (e, result) { 6972 if (e) { 6973 log.error(e, 'q.command(query-status): Error: ' + e.message); 6974 callback(e); 6975 return; 6976 } 6977 q.disconnect(); 6978 callback(null, result.hwsetup ? true : false); 6979 return; 6980 }); 6981 }); 6982 } 6983 6984 // cb (if set) will be called with an Error if we can't setup the interval loop 6985 // otherwise when the loop is shut down. 6986 // 6987 // vmobj must have: 6988 // 6989 // brand 6990 // state 6991 // uuid 6992 // zonepath 6993 // zoneroot 6994 // 6995 function markProvisionedWhenHWSetup(vmobj, options, cb) 6996 { 6997 var ival_handle; 6998 var log; 6999 var loop_interval = 3; // seconds 7000 var zoneroot; 7001 7002 log = options.log; 7003 assert(log, 'no logger passed to markProvisionedWenHWSetup()'); 7004 assert(vmobj.hasOwnProperty('zonepath'), 'no zonepath in vmobj'); 7005 7006 zoneroot = path.join(vmobj.zoneroot, '/root'); 7007 7008 if (!BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) { 7009 // do nothing for zones where we don't wait for hwsetup 7010 cb(new Error('brand ' + vmobj.brand + ' does not support hwsetup')); 7011 return (null); 7012 } 7013 7014 // Ensure the dataset doesn't have unsafe links as /var or /var/svc 7015 // Since we're checking the 'file' provision_success, this also guarantees 7016 // that if it already exists, it's not a symlink. 7017 try { 7018 assertSafeZonePath(zoneroot, '/var/svc/provision_success', 7019 {type: 'file', enoent_ok: true}); 7020 } catch (e) { 7021 cb(e); 7022 return (null); 7023 } 7024 7025 if (!options) { 7026 options = {}; 7027 } 7028 7029 // if caller wants they can change the interval 7030 if (options.hasOwnProperty('interval')) { 7031 loop_interval = options.interval; 7032 } 7033 7034 log.debug('setting hwsetup interval ' + vmobj.uuid); 7035 ival_handle = setInterval(function () { 7036 VM.load(vmobj.uuid, {fields: ['transition_expire', 'uuid'], log: log}, 7037 function (err, obj) { 7038 7039 var timeout_remaining; 7040 var ival = ival_handle; 7041 7042 function done() { 7043 if (ival_handle) { 7044 log.debug('clearing hwsetup interval ' + vmobj.uuid); 7045 clearInterval(ival); 7046 ival = null; 7047 } else { 7048 log.debug('done but no hwsetup interval ' + vmobj.uuid); 7049 } 7050 } 7051 7052 if (err) { 7053 // If the VM was deleted between calls, nothing much we can do. 7054 log.error(err, 'Unable to load ' + vmobj.uuid + ' ' 7055 + err.message); 7056 done(); 7057 cb(err); 7058 return; 7059 } 7060 7061 // we only do anything if we're still waiting for provisioning 7062 if (vmobj.state !== 'provisioning') { 7063 done(); 7064 cb(); 7065 return; 7066 } 7067 7068 timeout_remaining = 7069 (Number(obj.transition_expire) - Date.now(0)) / 1000; 7070 7071 if (timeout_remaining <= 0) { 7072 // IMPORTANT: this may run multiple times, must be idempotent 7073 7074 log.warn('Marking VM ' + vmobj.uuid + ' as "failed" because' 7075 + ' timeout expired and we are still "provisioning"'); 7076 VM.markVMFailure(vmobj, {log: log}, function (mark_err) { 7077 log.warn(mark_err, 'zoneinit failed, zone is ' 7078 + 'being stopped for manual investigation.'); 7079 done(); 7080 cb(); 7081 }); 7082 return; 7083 } 7084 7085 checkHWSetup(vmobj, log, function (check_err, result) { 7086 if (check_err) { 7087 log.debug(check_err, 'checkHWSetup Error: ' 7088 + check_err.message); 7089 return; 7090 } 7091 7092 if (result) { 7093 log.debug('QMP says VM ' + vmobj.uuid 7094 + ' completed hwsetup'); 7095 VM.unsetTransition(vmobj, {log: log}, function (unset_err) { 7096 var provisioning; 7097 var provision_success; 7098 7099 provisioning = path.join(vmobj.zonepath, 7100 '/root/var/svc/provisioning'); 7101 provision_success = path.join(vmobj.zonepath, 7102 '/root/var/svc/provision_success'); 7103 7104 if (unset_err) { 7105 log.error(unset_err); 7106 } else { 7107 log.debug('cleared transition to provisioning on' 7108 + ' ' + vmobj.uuid); 7109 } 7110 7111 fs.rename(provisioning, provision_success, 7112 function (e) { 7113 7114 if (e) { 7115 if (e.code === 'ENOENT') { 7116 log.debug(e); 7117 } else { 7118 log.error(e); 7119 } 7120 } 7121 7122 done(); 7123 cb(); 7124 return; 7125 }); 7126 }); 7127 } 7128 }); 7129 }); 7130 }, loop_interval * 1000); 7131 7132 return (ival_handle); 7133 } 7134 7135 function archiveVM(uuid, options, callback) 7136 { 7137 var archive_dirname; 7138 var dirmode; 7139 var log; 7140 var patterns_to_archive = []; 7141 var vmobj; 7142 7143 /*jsl:ignore*/ 7144 dirmode = 0755; 7145 /*jsl:end*/ 7146 7147 if (options.hasOwnProperty('log')) { 7148 log = options.log; 7149 } else { 7150 log = VM.log; 7151 } 7152 7153 log.debug('attempting to archive debug data for VM ' + uuid); 7154 7155 async.series([ 7156 function (cb) { 7157 // ensure directory exists 7158 archive_dirname = path.join('/zones/archive', uuid); 7159 7160 fs.mkdir(archive_dirname, dirmode, function (e) { 7161 log.debug(e, 'attempted to create ' + archive_dirname); 7162 cb(e); 7163 return; 7164 }); 7165 }, function (cb) { 7166 VM.load(uuid, {log: log}, function (err, obj) { 7167 if (err) { 7168 cb(err); 7169 return; 7170 } 7171 vmobj = obj; 7172 cb(); 7173 }); 7174 }, function (cb) { 7175 // write vmobj to archive 7176 var filename; 7177 7178 filename = path.join(archive_dirname, 'vm.json'); 7179 7180 fs.writeFile(filename, JSON.stringify(vmobj, null, 2) + '\n', 7181 function (err, result) { 7182 7183 if (err) { 7184 log.error(err, 'failed to create ' + filename + ': ' 7185 + err.message); 7186 } else { 7187 log.info('archived data to ' + filename); 7188 } 7189 7190 cb(); // ignore error 7191 }); 7192 }, function (cb) { 7193 var cmdline = '/usr/sbin/zfs list -t all -o name | grep ' 7194 + vmobj.zonename + ' | xargs zfs get -pH all >' 7195 + path.join(archive_dirname, 'zfs.dump'); 7196 7197 log.debug(cmdline); 7198 exec(cmdline, function (e, stdout, stderr) { 7199 if (e) { 7200 e.stdout = stdout; 7201 e.stderr = stderr; 7202 log.error({err: e}, 'failed to create ' 7203 + path.join(archive_dirname, 'zfs.dump')); 7204 cb(e); 7205 return; 7206 } 7207 log.info('archived data to ' + path.join(archive_dirname, 7208 'zfs.dump')); 7209 cb(); 7210 }); 7211 }, function (cb) { 7212 patterns_to_archive.push({ 7213 src: path.join('/etc/zones/', vmobj.zonename + '.xml'), 7214 dst: path.join(archive_dirname, 'zone.xml') 7215 }); 7216 patterns_to_archive.push({ 7217 src: path.join(vmobj.zonepath, 'config'), 7218 dst: archive_dirname, 7219 targ: path.join(archive_dirname, 'config') 7220 }); 7221 patterns_to_archive.push({ 7222 src: path.join(vmobj.zonepath, 'cores'), 7223 dst: archive_dirname, 7224 targ: path.join(archive_dirname, 'cores') 7225 }); 7226 7227 if (vmobj.brand === 'kvm') { 7228 patterns_to_archive.push({ 7229 src: path.join(vmobj.zonepath, 'root/tmp/vm*.log*'), 7230 dst: path.join(archive_dirname, 'vmlogs'), 7231 create_dst_dir: true 7232 }); 7233 patterns_to_archive.push({ 7234 src: path.join(vmobj.zonepath, 'root/startvm'), 7235 dst: archive_dirname, 7236 targ: path.join(archive_dirname, 'startvm') 7237 }); 7238 } else { 7239 patterns_to_archive.push({ 7240 src: path.join(vmobj.zonepath, 'root/var/svc/log/*'), 7241 dst: path.join(archive_dirname, 'svclogs'), 7242 create_dst_dir: true 7243 }); 7244 patterns_to_archive.push({ 7245 src: path.join(vmobj.zonepath, 'root/var/adm/messages*'), 7246 dst: path.join(archive_dirname, 'admmsgs'), 7247 create_dst_dir: true 7248 }); 7249 } 7250 7251 async.forEachSeries(patterns_to_archive, function (pattern, c) { 7252 7253 function cpPattern(p, cp_cb) { 7254 var cmdline = '/usr/bin/cp -RP ' + p.src + ' ' + p.dst; 7255 var targ = p.targ || p.dst; 7256 7257 log.debug(cmdline); 7258 exec(cmdline, function (e, stdout, stderr) { 7259 if (e) { 7260 e.stdout = stdout; 7261 e.stderr = stderr; 7262 log.error({err: e}, 'failed to archive data to ' 7263 + targ); 7264 } else { 7265 log.info('archived data to ' + targ); 7266 } 7267 // we don't return errors here because on error copying 7268 // one pattern we still want to grab the others. 7269 cp_cb(); 7270 }); 7271 } 7272 7273 if (pattern.create_dst_dir) { 7274 fs.mkdir(pattern.dst, dirmode, function (e) { 7275 if (!e) { 7276 log.info('created ' + pattern.dst); 7277 } else { 7278 log.error({err: e}, 'failed to create ' 7279 + pattern.dst); 7280 } 7281 cpPattern(pattern, c); 7282 }); 7283 } else { 7284 cpPattern(pattern, c); 7285 } 7286 }, function (e) { 7287 log.info('finished archiving VM ' + vmobj.uuid); 7288 cb(e); 7289 }); 7290 } 7291 ], function () { 7292 // XXX we ignore errors as failures to archive will not block VM delete. 7293 callback(); 7294 }); 7295 } 7296 7297 // vmobj argument should have: 7298 // 7299 // transition_to 7300 // uuid 7301 // zonename 7302 // 7303 exports.markVMFailure = function (vmobj, options, cb) 7304 { 7305 var log; 7306 7307 // options is optional 7308 if (arguments.length === 2) { 7309 cb = arguments[1]; 7310 options = {}; 7311 } 7312 7313 if (!vmobj || !vmobj.hasOwnProperty('uuid') 7314 || !vmobj.hasOwnProperty('zonename')) { 7315 7316 cb(new Error('markVMFailure needs uuid + zonename')); 7317 return; 7318 } 7319 7320 ensureLogging(true); 7321 if (options.hasOwnProperty('log')) { 7322 log = options.log; 7323 } else { 7324 log = VM.log.child({action: 'markVMFailure', vm: vmobj.uuid}); 7325 } 7326 7327 function dumpDebugInfo(zonename, callback) { 7328 var errors = {}; 7329 7330 async.series([ 7331 function (ptree_cb) { 7332 // note: if the zone is not running this returns empty but still 7333 // exits 0 7334 execFile('/usr/bin/ptree', ['-z', zonename], 7335 function (ptree_err, ptree_stdout, ptree_stderr) { 7336 7337 if (ptree_err) { 7338 log.error(ptree_err, 'unable to get ptree from ' 7339 + zonename + ': ' + ptree_stderr); 7340 errors.ptree_err = ptree_err; 7341 } else { 7342 log.warn('processes running in ' + zonename 7343 + ' at fail time:\n' + ptree_stdout); 7344 } 7345 7346 ptree_cb(); // don't fail on error here. 7347 } 7348 ); 7349 }, function (svcs_cb) { 7350 execFile('/usr/bin/svcs', ['-xv', '-z', zonename], 7351 function (svcs_err, svcs_stdout, svcs_stderr) { 7352 7353 if (svcs_err) { 7354 log.error(svcs_err, 'unable to get svcs from ' 7355 + zonename + ': ' + svcs_stderr); 7356 errors.svcs_err = svcs_err; 7357 } else { 7358 log.warn('svcs -xv output for ' + zonename 7359 + ' at fail time:\n' + svcs_stdout); 7360 } 7361 7362 svcs_cb(); // don't fail on error here. 7363 } 7364 ); 7365 }, function (kstat_cb) { 7366 execFile('/usr/bin/kstat', ['-n', zonename.substr(0, 30)], 7367 function (kstat_err, kstat_stdout, kstat_stderr) { 7368 7369 if (kstat_err) { 7370 log.error(kstat_err, 'unable to get kstats from ' 7371 + zonename + ': ' + kstat_stderr); 7372 errors.kstat_err = kstat_err; 7373 } else { 7374 log.warn('kstat output for ' + zonename 7375 + ' at fail time:\n' + kstat_stdout); 7376 } 7377 7378 kstat_cb(); // don't fail on error here. 7379 } 7380 ); 7381 } 7382 ], function () { 7383 callback(errors); 7384 }); 7385 } 7386 7387 dumpDebugInfo(vmobj.zonename, function (debug_err) { 7388 var zcfg; 7389 7390 // note: we don't treat failure to dump debug info as a fatal error. 7391 log.warn(debug_err, 'zone setup failed, zone is being stopped ' 7392 + 'for manual investigation.'); 7393 7394 // Mark the zone as 'failed' 7395 zcfg = 'remove -F attr name=failed; add attr; set name=failed; ' 7396 + 'set value="provisioning"; set type=string; end'; 7397 7398 zonecfg(['-u', vmobj.uuid, zcfg], log, function (zonecfg_err, fds) { 7399 7400 if (zonecfg_err) { 7401 log.error({err: zonecfg_err, stdout: fds.stdout, 7402 stderr: fds.stderr}, 'Unable to set failure flag on ' 7403 + vmobj.uuid + ': ' + zonecfg_err.message); 7404 } else { 7405 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 7406 'set failure flag on ' + vmobj.uuid); 7407 } 7408 7409 // attempt to remove transition 7410 VM.unsetTransition(vmobj, {log: log}, function (unset_err) { 7411 if (unset_err) { 7412 log.error(unset_err); 7413 } 7414 7415 VM.stop(vmobj.uuid, {force: true, log: log}, 7416 function (stop_err) { 7417 7418 // only log errors because there's nothing to do 7419 7420 if (stop_err) { 7421 log.error(stop_err, 'failed to stop VM ' 7422 + vmobj.uuid + ': ' + stop_err.message); 7423 } 7424 7425 cb(); 7426 }); 7427 }); 7428 }); 7429 }); 7430 }; 7431 7432 function svccfg(zonepath, args, log, callback) 7433 { 7434 var cmd = '/usr/sbin/svccfg'; 7435 var exec_options = {}; 7436 var zoneroot = path.join(zonepath, '/root'); 7437 7438 assert(log, 'no logger passed to svccfg()'); 7439 7440 try { 7441 assertSafeZonePath(zoneroot, '/etc/svc/repository.db', 7442 {type: 'file', enoent_ok: false}); 7443 } catch (e) { 7444 log.error(e, 'Error validating /etc/svc/repository.db: ' + e.message); 7445 callback(e); 7446 return; 7447 } 7448 7449 exec_options = { 7450 env: { 7451 'SVCCFG_CONFIGD_PATH': '/lib/svc/bin/svc.configd', 7452 'SVCCFG_REPOSITORY': 7453 path.join(zonepath, 'root', '/etc/svc/repository.db') 7454 } 7455 }; 7456 7457 log.debug({'command': cmd + ' ' + args.join(' '), 7458 'exec_options': exec_options}, 'modifying svc repo in ' + zonepath); 7459 execFile(cmd, args, exec_options, function (error, stdout, stderr) { 7460 if (error) { 7461 callback(error, {'stdout': stdout, 'stderr': stderr}); 7462 } else { 7463 callback(null, {'stdout': stdout, 'stderr': stderr}); 7464 } 7465 }); 7466 } 7467 7468 // This calls cb() when /var/svc/provisioning is gone. When this calls cb() 7469 // with an Error object, the provision is considered failed so this should 7470 // only happen when something timed out that is unrelated to the user. 7471 // 7472 // This returns a function that can be called with no arguments to cancel 7473 // all timers and actions pending from this function. It will also then not 7474 // call the cb(). 7475 // 7476 // IMPORTANT: this is only exported to be used by vmadmd. Do not use elsewhere! 7477 // 7478 // vmobj fields: 7479 // 7480 // state 7481 // transition_expire 7482 // uuid 7483 // zonepath 7484 // 7485 exports.waitForProvisioning = function (vmobj, options, cb) 7486 { 7487 var dirname = path.join(vmobj.zonepath, 'root', '/var/svc'); 7488 var filename = path.join(dirname, 'provisioning'); 7489 var ival_h; 7490 var log; 7491 var timeout; 7492 var timeout_remaining = PROVISION_TIMEOUT; // default to whole thing 7493 var watcher; 7494 7495 // options is optional 7496 if (arguments.length === 2) { 7497 cb = arguments[1]; 7498 options = {}; 7499 } 7500 7501 ensureLogging(true); 7502 if (options.hasOwnProperty('log')) { 7503 log = options.log; 7504 } else { 7505 log = VM.log.child({action: 'waitForProvisioning', vm: vmobj.uuid}); 7506 } 7507 7508 function done() { 7509 if (timeout) { 7510 log.debug('clearing provision timeout for ' + vmobj.uuid); 7511 clearTimeout(timeout); 7512 timeout = null; 7513 } 7514 if (watcher) { 7515 log.debug('closing /var/svc/provisioning watcher for ' 7516 + vmobj.uuid); 7517 watcher.close(); 7518 watcher = null; 7519 } 7520 if (ival_h) { 7521 log.debug('closing hwsetup check interval for ' + vmobj.uuid); 7522 clearInterval(ival_h); 7523 ival_h = null; 7524 } 7525 } 7526 7527 if ((vmobj.state === 'provisioning') 7528 && (vmobj.hasOwnProperty('transition_expire'))) { 7529 7530 timeout_remaining = 7531 (Number(vmobj.transition_expire) - Date.now(0)) / 1000; 7532 7533 // Always give it at least 1 second's chance. 7534 if (timeout_remaining < 1) { 7535 timeout_remaining = 1; 7536 } 7537 } else { 7538 // don't know what to do here we're not provisioning. 7539 log.warn('waitForProvisioning called when ' + vmobj.uuid 7540 + ' was not provisioning'); 7541 cb(); 7542 return (null); 7543 } 7544 7545 log.debug({ 7546 'transition_expire': Number(vmobj.transition_expire), 7547 'now': Date.now(0) 7548 }, 'waiting ' + timeout_remaining + ' sec(s) for provisioning'); 7549 7550 log.debug('setting provision timeout for ' + vmobj.uuid); 7551 timeout = setTimeout(function () { 7552 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure" because we ' 7553 + 'hit waitForProvisioning() timeout.'); 7554 VM.markVMFailure(vmobj, {log: log}, function (err) { 7555 var errstr = 'timed out waiting for /var/svc/provisioning to move' 7556 + ' for ' + vmobj.uuid; 7557 if (err) { 7558 log.warn(err, 'markVMFailure(): ' + err.message); 7559 } 7560 log.error(errstr); 7561 done(); 7562 cb(new Error(errstr)); 7563 }); 7564 }, (timeout_remaining * 1000)); 7565 7566 // this starts a loop that will move provisioning -> provision_success when 7567 // the hardware of the VM has been initialized the first time. 7568 if (BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) { 7569 ival_h = markProvisionedWhenHWSetup(vmobj, {log: log}, function (err) { 7570 if (err) { 7571 log.error(err, 'error in markProvisionedWhenHWSetup()'); 7572 } 7573 done(); 7574 cb(err); 7575 }); 7576 return (done); 7577 } 7578 7579 watcher = fs.watch(filename, function (evt, file) { 7580 // We only care about 'rename' which also fires when the file is 7581 // deleted. 7582 log.debug('watcher.event(' + vmobj.uuid + '): ' + evt); 7583 if (evt === 'rename') { 7584 fs.exists(filename, function (exists) { 7585 if (exists) { 7586 // somehow we still have /var/svc/provisioning! 7587 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure"' 7588 + ' because we still have /var/svc/provisioning after ' 7589 + 'rename'); 7590 VM.markVMFailure(vmobj, {log: log}, function (err) { 7591 if (err) { 7592 log.warn(err, 'markVMFailure(): ' + err.message); 7593 } 7594 done(); 7595 cb(new Error('/var/svc/provisioning exists after ' 7596 + 'rename!')); 7597 }); 7598 return; 7599 } 7600 7601 // So long as /var/svc/provisioning is gone, we don't care what 7602 // replaced it. Success or failure of user script doesn't 7603 // matter for the state, it's provisioned now. Caller should 7604 // now clear the transition. 7605 done(); 7606 cb(); 7607 return; 7608 }); 7609 } 7610 }); 7611 7612 log.debug('created watcher for ' + vmobj.uuid); 7613 return (done); 7614 }; 7615 7616 // create and install a 'joyent' or 'kvm' brand zone. 7617 function installZone(payload, log, callback) 7618 { 7619 var load_fields; 7620 var receiving = false; 7621 var reprovisioning = false; 7622 var vmobj; 7623 var zoneinit = {}; 7624 7625 assert(log, 'no logger passed to installZone()'); 7626 7627 log.debug('installZone()'); 7628 7629 load_fields = [ 7630 'brand', 7631 'firewall_enabled', 7632 'missing', 7633 'nics', 7634 'owner_uuid', 7635 'routes', 7636 'state', 7637 'tags', 7638 'transition_to', 7639 'transition_expire', 7640 'uuid', 7641 'zonename', 7642 'zonepath' 7643 ]; 7644 7645 if (payload.reprovisioning) { 7646 log.debug('installZone(): reprovisioning'); 7647 reprovisioning = true; 7648 } 7649 7650 async.series([ 7651 function (cb) { 7652 7653 VM.load(payload.uuid, {fields: load_fields, log: log}, 7654 function (err, obj) { 7655 7656 if (err) { 7657 cb(err); 7658 return; 7659 } 7660 vmobj = obj; 7661 cb(); 7662 }); 7663 }, function (cb) { 7664 var thing; 7665 var missing = false; 7666 var msg; 7667 var things = ['datasets', 'filesystems', 'disks']; 7668 7669 if (vmobj.state === 'receiving') { 7670 receiving = true; 7671 msg = 'zone is still missing:'; 7672 for (thing in things) { 7673 thing = things[thing]; 7674 if (vmobj.missing[thing].length !== 0) { 7675 msg = msg + ' ' + vmobj.missing[thing].length + ' ' 7676 + thing + ','; 7677 missing = true; 7678 } 7679 } 7680 msg = rtrim(msg, ','); 7681 7682 if (missing) { 7683 cb(new Error('Unable to complete install for ' 7684 + vmobj.uuid + ' ' + msg)); 7685 return; 7686 } 7687 } 7688 cb(); 7689 }, function (cb) { 7690 // Install the zone. 7691 // This will create the dataset and mark the zone 'installed'. 7692 var args; 7693 7694 if (reprovisioning) { 7695 // reprovisioning we do *most* of install, but not this. 7696 cb(); 7697 return; 7698 } 7699 7700 args = ['-z', vmobj.zonename, 'install', '-q', 7701 payload.quota.toString()]; 7702 7703 // For both OS and KVM VMs you can pass an image_uuid at the 7704 // top-level. This will be your zone's root dataset. On KVM the user 7705 // is never exposed to this. It's used there for something like 7706 // SPICE. 7707 if (payload.hasOwnProperty('image_uuid')) { 7708 args.push('-t', payload.image_uuid, '-x', 'nodataset'); 7709 } 7710 7711 zoneadm(args, log, function (err, fds) { 7712 if (err) { 7713 log.error({err: err, stdout: fds.stdout, 7714 stderr: fds.stderr}, 'zoneadm failed to install: ' 7715 + err.message); 7716 cb(err); 7717 } else { 7718 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 7719 'zoneadm installed zone'); 7720 cb(); 7721 } 7722 }); 7723 }, function (cb) { 7724 // Apply compression if set 7725 var args = []; 7726 if (payload.hasOwnProperty('zfs_root_compression')) { 7727 args = ['set', 'compression=' 7728 + payload.zfs_root_compression, payload.zfs_filesystem]; 7729 zfs(args, log, function (err) { 7730 cb(err); 7731 }); 7732 } else { 7733 cb(); 7734 } 7735 }, function (cb) { 7736 // Apply recsize if set 7737 var args = []; 7738 if (payload.hasOwnProperty('zfs_root_recsize')) { 7739 args = ['set', 'recsize=' + payload.zfs_root_recsize, 7740 payload.zfs_filesystem]; 7741 zfs(args, log, function (err) { 7742 cb(err); 7743 }); 7744 } else { 7745 cb(); 7746 } 7747 }, function (cb) { 7748 // Some zones can have an additional 'data' dataset delegated to 7749 // them for use in the zone. This will set that up. If the option 7750 // is not set, the following does nothing. 7751 if (!receiving && !reprovisioning) { 7752 createDelegatedDataset(payload, log, function (err) { 7753 if (err) { 7754 cb(err); 7755 } else { 7756 cb(); 7757 } 7758 }); 7759 } else { 7760 cb(); 7761 } 7762 }, function (cb) { 7763 // Write out the zone's metadata 7764 // Note: we don't do this when receiving because dataset will 7765 // already contain metadata and we don't want to wipe that out. 7766 if (!receiving && !reprovisioning) { 7767 saveMetadata(payload, log, function (err) { 7768 if (err) { 7769 log.error(err, 'unable to save metadata: ' 7770 + err.message); 7771 cb(err); 7772 } else { 7773 cb(); 7774 } 7775 }); 7776 } else { 7777 cb(); 7778 } 7779 }, function (cb) { 7780 // Write out the zone's routes 7781 // Note: we don't do this when receiving because dataset will 7782 // already contain routes and we don't want to wipe that out. 7783 if (!receiving && !reprovisioning) { 7784 saveRoutes(payload, log, function (err) { 7785 if (err) { 7786 log.error(err, 'unable to save routes: ' 7787 + err.message); 7788 cb(err); 7789 } else { 7790 cb(); 7791 } 7792 }); 7793 } else { 7794 cb(); 7795 } 7796 }, function (cb) { 7797 // if we were receiving, we're done receiving now 7798 if (receiving) { 7799 VM.unsetTransition(vmobj, {log: log}, cb); 7800 } else { 7801 cb(); 7802 } 7803 }, function (cb) { 7804 // var zoneinit is in installZone() scope 7805 7806 // when receiving zoneinit is never run. 7807 if (receiving) { 7808 cb(); 7809 return; 7810 } 7811 7812 getZoneinitJSON(vmobj.zonepath, log, function (zoneinit_err, data) { 7813 7814 if (zoneinit_err) { 7815 // NOTE: not existing is not going to give us a zoneinit_err 7816 log.warn(zoneinit_err, 'error in getZoneinitJSON'); 7817 cb(zoneinit_err); 7818 return; 7819 } 7820 7821 if (data) { 7822 zoneinit = data; 7823 } else { 7824 zoneinit = {}; 7825 } 7826 7827 cb(); 7828 }); 7829 }, function (cb) { 7830 // var_svc_provisioning is at installZone() scope 7831 7832 // If we're not receiving, we're provisioning a new VM and in that 7833 // case we write the /var/svc/provisioning file which should exist 7834 // until something in the zone decides provisioning is complete. At 7835 // that point it will be moved to either: 7836 // 7837 // /var/svc/provision_success 7838 // /var/svc/provision_failure 7839 // 7840 // to indicate that the provisioning setup has been completed. 7841 7842 if (receiving) { 7843 cb(); 7844 return; 7845 } 7846 7847 fs.writeFile(path.join(vmobj.zonepath, 'root', 7848 '/var/svc/provisioning'), '', function (err, result) { 7849 7850 if (err) { 7851 log.error(err, 'failed to create ' 7852 + '/var/svc/provisioning: ' + err.message); 7853 } else { 7854 log.debug('created /var/svc/provisioning in ' 7855 + path.join(vmobj.zonepath, 'root')); 7856 } 7857 7858 cb(err); 7859 }); 7860 }, function (cb) { 7861 // For joyent and joyent-minimal at least, set the timeout for the 7862 // svc start method to the value specified in the payload, or a 7863 // default. 7864 7865 var timeout; 7866 7867 if (BRAND_OPTIONS[vmobj.brand].features.update_mdata_exec_timeout) { 7868 7869 if (payload.hasOwnProperty('mdata_exec_timeout')) { 7870 timeout = payload.mdata_exec_timeout; 7871 } else { 7872 timeout = DEFAULT_MDATA_TIMEOUT; 7873 } 7874 7875 svccfg(vmobj.zonepath, [ 7876 '-s', 'svc:/smartdc/mdata:execute', 7877 'setprop', 'start/timeout_seconds', '=', 'count:', timeout 7878 ], log, function (error, stdio) { 7879 7880 if (error) { 7881 log.error(error, 'failed to set mdata:exec timeout'); 7882 cb(error); 7883 return; 7884 } 7885 7886 cb(); 7887 }); 7888 } else { 7889 cb(); 7890 } 7891 7892 }, function (cb) { 7893 // This writes out the 'zoneconfig' file used by zoneinit to root's 7894 // home directory in the zone. 7895 if (! receiving 7896 && BRAND_OPTIONS[vmobj.brand].features.zoneinit 7897 && (! zoneinit.hasOwnProperty('features') 7898 || zoneinit.features.zoneconfig)) { 7899 7900 // No 'features' means old dataset. If we have old dataset or 7901 // one that really wants a zoneconfig, write it out. 7902 7903 writeZoneconfig(payload, log, function (err) { 7904 cb(err); 7905 }); 7906 } else { 7907 cb(); 7908 } 7909 }, function (cb) { 7910 if (BRAND_OPTIONS[vmobj.brand].features.write_zone_netfiles 7911 && !receiving) { 7912 7913 writeZoneNetfiles(payload, log, function (err) { 7914 cb(err); 7915 }); 7916 } else { 7917 cb(); 7918 } 7919 }, function (cb) { 7920 if (vmobj.hasOwnProperty('zonepath') 7921 && BRAND_OPTIONS[vmobj.brand].features.cleanup_dataset 7922 && !receiving) { 7923 7924 cleanupMessyDataset(vmobj.zonepath, vmobj.brand, log, 7925 function (err) { 7926 7927 cb(err); 7928 }); 7929 } else { 7930 cb(); 7931 } 7932 }, function (cb) { 7933 // Firewall data has not changed when reprovisioning, so we don't 7934 // re-run addFirewallData() 7935 if (reprovisioning) { 7936 cb(); 7937 return; 7938 } 7939 7940 // Add firewall data if it was included 7941 addFirewallData(payload, vmobj, log, cb); 7942 }, function (cb) { 7943 7944 var cancel; 7945 var calledback = false; 7946 var prov_wait = true; 7947 // var_svc_provisioning is at installZone() scope 7948 7949 // The vm is now ready to start, we'll start if autoboot is set. If 7950 // not, we also don't want to wait for 'provisioning'. 7951 if (!payload.autoboot) { 7952 cb(); 7953 return; 7954 } 7955 7956 // In these cases we never wait for provisioning -> running 7957 if (payload.nowait || receiving || vmobj.state !== 'provisioning') { 7958 prov_wait = false; 7959 } 7960 7961 // most VMs support the /var/svc/provision{ing,_success,_failure} 7962 // files. For those, if !nowait, we wait for the file to change 7963 // from provisioning -> either provision_success, or 7964 // provision_failure. 7965 7966 if (prov_wait) { 7967 // wait for /var/svc/provisioning -> provision_success/failure 7968 cancel = VM.waitForProvisioning(vmobj, {log: log}, 7969 function (err) { 7970 7971 log.debug(err, 'waited for provisioning'); 7972 7973 if (!err) { 7974 log.info('provisioning complete: ' 7975 + '/var/svc/provisioning is gone'); 7976 // this will clear the provision transition 7977 VM.unsetTransition(vmobj, {log: log}, 7978 function (unset_err) { 7979 7980 if (unset_err) { 7981 log.error(unset_err, 'error unsetting ' 7982 + 'transition: ' + unset_err.message); 7983 } 7984 // this and the cb in the VM.start callback might 7985 // both run if we don't check this. 7986 if (!calledback) { 7987 calledback = true; 7988 cb(unset_err); 7989 } 7990 }); 7991 } else { 7992 // failed but might not be able to cb if VM.start's 7993 // callback already did. 7994 log.error(err, 'error waiting for provisioning: ' 7995 + err.message); 7996 // this and the cb in the VM.start callback might 7997 // both run if we don't check this. 7998 if (!calledback) { 7999 calledback = true; 8000 cb(err); 8001 } 8002 } 8003 }); 8004 } 8005 8006 VM.start(payload.uuid, {}, {log: log}, function (err, res) { 8007 if (err) { 8008 // we failed to start so we'll never see provisioning, so 8009 // cancel that and return the error. 8010 if (cancel) { 8011 log.info('cancelling VM.waitForProvisioning'); 8012 cancel(); 8013 } 8014 // this and the cb in the VM.waitForProvisioning 8015 // callback might both run if we don't check this. 8016 if (!calledback) { 8017 calledback = true; 8018 cb(err); 8019 } 8020 return; 8021 } 8022 // if we're waiting for 'provisioning' VM.waitForProvisioning's 8023 // callback will call cb(). If we're not going to wait, we call 8024 // it here. 8025 if (!prov_wait) { 8026 // this and the cb in the VM.waitForProvisioning 8027 // callback might both run if we don't check this. 8028 if (!calledback) { 8029 calledback = true; 8030 cb(); 8031 } 8032 } 8033 }); 8034 }], function (error) { 8035 callback(error); 8036 } 8037 ); 8038 } 8039 8040 function getZoneinitJSON(rootpath, log, cb) 8041 { 8042 var filename; 8043 var zoneroot; 8044 8045 assert(log, 'no logger passed to getZoneinitJSON()'); 8046 8047 zoneroot = path.join('/', rootpath, 'root'); 8048 filename = path.join(zoneroot, '/var/zoneinit/zoneinit.json'); 8049 8050 try { 8051 assertSafeZonePath(zoneroot, '/var/zoneinit/zoneinit.json', 8052 {type: 'file', enoent_ok: true}); 8053 } catch (e) { 8054 log.error(e, 'Error validating /var/zoneinit/zoneinit.json: ' 8055 + e.message); 8056 cb(e); 8057 return; 8058 } 8059 8060 fs.readFile(filename, function (error, data) { 8061 var zoneinit; 8062 8063 if (error && (error.code === 'ENOENT')) { 8064 // doesn't exist, leave empty 8065 log.debug('zoneinit.json does not exist.'); 8066 cb(); 8067 } else if (error) { 8068 // error reading: fail. 8069 cb(error); 8070 } else { 8071 // success try to load json 8072 try { 8073 zoneinit = JSON.parse(data.toString()); 8074 log.debug({'zoneinit_json': zoneinit}, 8075 'parsed zoneinit.json'); 8076 cb(null, zoneinit); 8077 } catch (e) { 8078 cb(e); 8079 } 8080 } 8081 }); 8082 } 8083 8084 function getDatasetMountpoint(dataset, log, callback) 8085 { 8086 var args; 8087 var cmd = '/usr/sbin/zfs'; 8088 var mountpoint; 8089 8090 assert(log, 'no logger passed to getDatasetMountpoint()'); 8091 8092 args = ['get', '-H', '-o', 'value', 'mountpoint', dataset]; 8093 8094 log.debug(cmd + ' ' + args.join(' ')); 8095 execFile(cmd, args, function (error, stdout, stderr) { 8096 if (error) { 8097 log.error(error, 'zfs get failed with: ' + stderr); 8098 callback(error); 8099 } else { 8100 mountpoint = stdout.replace(/\n/g, ''); 8101 log.debug('mountpoint: "' + mountpoint + '"'); 8102 callback(null, mountpoint); 8103 } 8104 }); 8105 } 8106 8107 // TODO: pull data out of the massive zfs list we pulled earlier 8108 function checkDatasetProvisionable(payload, log, callback) 8109 { 8110 var dataset; 8111 8112 assert(log, 'no logger passed to checkDatasetProvisionable()'); 8113 8114 if (BRAND_OPTIONS[payload.brand].features.var_svc_provisioning) { 8115 // when the brand always supports /var/svc/provisioning we don't have to 8116 // worry about the dataset not supporting it. 8117 callback(true); 8118 return; 8119 } 8120 8121 if (!payload.hasOwnProperty('zpool') 8122 || !payload.hasOwnProperty('image_uuid')) { 8123 8124 log.error('missing properties required to find dataset: ' 8125 + JSON.stringify(payload)); 8126 callback(false); 8127 return; 8128 } 8129 8130 dataset = payload.zpool + '/' + payload.image_uuid; 8131 8132 getDatasetMountpoint(dataset, log, function (dataset_err, mountpoint) { 8133 if (dataset_err) { 8134 log.error('unable to find mount point for ' + dataset); 8135 callback(false); 8136 return; 8137 } 8138 8139 getZoneinitJSON(dataset, log, function (zoneinit_err, zoneinit) { 8140 var filename_1_6_x; 8141 var filename_1_8_x; 8142 8143 if (zoneinit_err) { 8144 log.error(zoneinit_err, 'getZoneinitJSON() failed, assuming ' 8145 + 'not provisionable.'); 8146 callback(false); 8147 return; 8148 } else if (!zoneinit) { 8149 log.debug('no data from getZoneinitJSON(), using {}'); 8150 zoneinit = {}; 8151 } 8152 8153 if (zoneinit.hasOwnProperty('features')) { 8154 if (zoneinit.features.var_svc_provisioning) { 8155 log.info('zoneinit.features.var_svc_provisioning is ' 8156 + 'set.'); 8157 callback(true); 8158 return; 8159 } 8160 // we have features but not var_svc_provisioning === true means 8161 // we can't provision. Fall through and return false. 8162 } else { 8163 // Didn't load zoneinit features, so check for datasets that 8164 // have // 04-mdata.sh. For 1.6.x and earlier datasets this was 8165 // in /root but in 1.8.0 and 1.8.1 it is in /var/zoneinit. For 8166 // 1.8.2 and later we'll not get here as the zoneinit.json will 8167 // exist and we'll use that. 8168 filename_1_6_x = path.join(mountpoint, 'root', 8169 '/root/zoneinit.d/04-mdata.sh'); 8170 filename_1_8_x = path.join(mountpoint, 'root', 8171 '/var/zoneinit/includes/04-mdata.sh'); 8172 8173 if (fs.existsSync(filename_1_6_x)) { 8174 log.info(filename_1_6_x + ' exists'); 8175 callback(true); 8176 return; 8177 } else { 8178 log.debug(filename_1_6_x + ' does not exist'); 8179 if (fs.existsSync(filename_1_8_x)) { 8180 log.info(filename_1_8_x + ' exists'); 8181 callback(true); 8182 return; 8183 } else { 8184 log.debug(filename_1_8_x + ' does not exist'); 8185 // this was our last chance. 8186 // Fall through and return false. 8187 } 8188 } 8189 } 8190 8191 callback(false); 8192 return; 8193 }); 8194 }); 8195 } 8196 8197 // create and install a 'joyent' or 'kvm' brand zone. 8198 function createZone(payload, log, callback) 8199 { 8200 var create_time; 8201 var n; 8202 var now = new Date; 8203 var primary_found; 8204 var provision_timeout = PROVISION_TIMEOUT; 8205 var t; 8206 var vm_version; 8207 var zcfg; 8208 8209 assert(log, 'no logger passed to createZone()'); 8210 8211 log.debug('createZone()'); 8212 8213 payload.zfs_filesystem = payload.zpool + '/' + payload.zonename; 8214 payload.zonepath = '/' + payload.zfs_filesystem; 8215 8216 // we add create-timestamp in all cases except where we're receiving since 8217 // in that case we want to preserve the original create-timestamp. 8218 if (!payload.hasOwnProperty('transition') 8219 || (payload.transition.transition !== 'receiving') 8220 || !payload.hasOwnProperty('create_timestamp')) { 8221 8222 create_time = now.toISOString(); 8223 } else { 8224 create_time = payload.create_timestamp; 8225 } 8226 8227 // we add vm-version (property v) in all cases except where we're receiving 8228 // since in that case we want to preserve the original version. 8229 if (!payload.hasOwnProperty('transition') 8230 || (payload.transition.transition !== 'receiving') 8231 || !payload.hasOwnProperty('v')) { 8232 8233 vm_version = 1; 8234 } else { 8235 vm_version = payload.v; 8236 } 8237 8238 // set the properties that can't be updated later here. 8239 zcfg = 'create -b\n' 8240 + 'set zonepath=' + payload.zonepath + '\n' 8241 + 'set brand=' + payload.brand + '\n' 8242 + 'set uuid=' + payload.uuid + '\n' 8243 + 'set ip-type=exclusive\n' 8244 + 'add attr; set name="vm-version"; set type=string; set value="' 8245 + vm_version + '"; end\n' 8246 + 'add attr; set name="create-timestamp"; set type=string; set value="' 8247 + create_time + '"; end\n'; 8248 8249 if (payload.hasOwnProperty('transition')) { 8250 // IMPORTANT: this is for internal use only and should not be documented 8251 // as an option for create's payload. Used for receive. 8252 t = payload.transition; 8253 zcfg = zcfg 8254 + buildTransitionZonecfg(t.transition, t.target, t.timeout) + '\n'; 8255 } else { 8256 // Assume this is really a new VM, add transition called 'provisioning' 8257 // only if the machine is going to be booting. 8258 if (!payload.hasOwnProperty('autoboot') || payload.autoboot) { 8259 zcfg = zcfg + buildTransitionZonecfg('provisioning', 'running', 8260 provision_timeout * 1000) + '\n'; 8261 } 8262 } 8263 8264 // We call the property 'dataset-uuid' even though the property name is 8265 // image_uuid because existing VMs in the wild will be using dataset-uuid 8266 // already, and we are the point where the image becomes a dataset anyway. 8267 if (payload.hasOwnProperty('image_uuid')) { 8268 zcfg = zcfg + 'add attr; set name="dataset-uuid"; set type=string; ' 8269 + 'set value="' + payload.image_uuid + '"; end\n'; 8270 } 8271 8272 if (BRAND_OPTIONS[payload.brand].features.use_vm_autoboot) { 8273 // we always set autoboot=false for VM zones, since we want vmadmd to 8274 // boot them and not the zones tools. Use vm-autoboot to control VMs 8275 zcfg = zcfg + 'set autoboot=false\n'; 8276 } 8277 8278 // ensure that we have a primary nic, even if one wasn't specified 8279 if (payload.hasOwnProperty('add_nics') && payload.add_nics.length != 0) { 8280 primary_found = false; 8281 8282 for (n in payload.add_nics) { 8283 n = payload.add_nics[n]; 8284 if (n.hasOwnProperty('primary') && n.primary) { 8285 primary_found = true; 8286 break; 8287 } 8288 } 8289 if (!primary_found) { 8290 payload.add_nics[0].primary = true; 8291 } 8292 } 8293 8294 // Passing an empty first parameter here, tells buildZonecfgUpdate that 8295 // we're talking about a new machine. 8296 zcfg = zcfg + buildZonecfgUpdate({}, payload, log); 8297 8298 // include the zonecfg in the debug output to help track down problems. 8299 log.debug(zcfg); 8300 8301 // send the zonecfg data we just generated as a file to zonecfg, 8302 // this will create the zone. 8303 zonecfgFile(zcfg, ['-z', payload.zonename], log, function (err, fds) { 8304 if (err || payload.create_only) { 8305 log.error({err: err, zcfg: zcfg, stdout: fds.stdout, 8306 stderr: fds.stderr}, 'failed to modify zonecfg'); 8307 callback(err); 8308 } else { 8309 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8310 'modified zonecfg'); 8311 installZone(payload, log, callback); 8312 } 8313 }); 8314 } 8315 8316 function normalizeNics(payload, vmobj) 8317 { 8318 var n; 8319 var nic; 8320 8321 // ensure all NICs being created/added have a MAC, remove the 'index' if it 8322 // is passed (that's deprecated), rename 'interface' to 'physical'. 8323 if (payload.hasOwnProperty('add_nics')) { 8324 for (n in payload.add_nics) { 8325 if (payload.add_nics.hasOwnProperty(n)) { 8326 nic = payload.add_nics[n]; 8327 8328 if (!nic.hasOwnProperty('mac') 8329 && !nic.hasOwnProperty('vrrp_vrid')) { 8330 nic.mac = generateMAC(); 8331 } 8332 delete nic.index; 8333 if (nic.hasOwnProperty('interface')) { 8334 nic.physical = nic.interface; 8335 delete nic.interface; 8336 } 8337 8338 // nics.*.primary only supports true value, unset false. We also 8339 // handle the case here why they used the deprecated '1' value. 8340 // We will have already warned them, but still support for now. 8341 if (nic.hasOwnProperty('primary')) { 8342 if (nic.primary || nic.primary === '1' 8343 || nic.primary === 1) { 8344 8345 nic.primary = true; 8346 } else { 8347 delete nic.primary; 8348 } 8349 } 8350 } 8351 } 8352 } 8353 } 8354 8355 /* 8356 * This is called for both create and update, everything here should be safe for 8357 * both. The vmobj will be set if it's an update. 8358 * 8359 */ 8360 function normalizePayload(payload, vmobj, log, callback) 8361 { 8362 var action; 8363 var allowed; 8364 var brand; 8365 var property; 8366 8367 assert(log, 'no logger passed to normalizePayload()'); 8368 8369 // fix type of arguments that should be numbers, do this here so that fixing 8370 // memory works correctly later using math. 8371 for (property in payload) { 8372 if (payload.hasOwnProperty(property)) { 8373 if (PAYLOAD_PROPERTIES.hasOwnProperty(property) 8374 && PAYLOAD_PROPERTIES[property].type === 'integer' 8375 && payload[property] !== undefined) { 8376 // undefined is a special case since we use that to unset props 8377 8378 payload[property] = Number(payload[property]); 8379 if (isNaN(payload[property])) { 8380 callback(new Error('Invalid value for ' + property + ': ' 8381 + JSON.stringify(payload[property]) + ':' 8382 + typeof (payload[property]))); 8383 return; 8384 } 8385 } 8386 } 8387 } 8388 8389 if (payload.hasOwnProperty('quota') && payload.quota === undefined) { 8390 // when unsetting quota we set to 0 8391 payload.quota = 0; 8392 } 8393 8394 if (vmobj) { 8395 /* update */ 8396 fixPayloadMemory(payload, vmobj, log); 8397 action = 'update'; 8398 } else { 8399 /* this also calls fixPayloadMemory() */ 8400 applyZoneDefaults(payload, log); 8401 8402 if (payload.hasOwnProperty('create_only') 8403 && payload.transition.transition === 'receiving') { 8404 8405 action = 'receive'; 8406 } else { 8407 action = 'create'; 8408 } 8409 } 8410 8411 // Should always have a brand after we applied defaults. 8412 if (vmobj && vmobj.hasOwnProperty('brand')) { 8413 brand = vmobj.brand; 8414 } else if (payload.hasOwnProperty('brand')) { 8415 brand = payload.brand; 8416 } else { 8417 callback(new Error('Unable to determine brand for payload')); 8418 return; 8419 } 8420 8421 // Historically we supported dataset_uuid for joyent+joyent-minimal and 8422 // zone_dataset_uuid for kvm. Now we just support image_uuid so give a 8423 // deprecation warning and translate if old version specified. This needs 8424 // to happen before VM.validate because image_uuid is required for most 8425 // VMs. 8426 allowed = BRAND_OPTIONS[brand].allowed_properties; 8427 if ((allowed.hasOwnProperty('dataset_uuid') 8428 && payload.hasOwnProperty('dataset_uuid')) 8429 || (allowed.hasOwnProperty('zone_dataset_uuid') 8430 && payload.hasOwnProperty('zone_dataset_uuid'))) { 8431 8432 property = (payload.hasOwnProperty('dataset_uuid') ? 'dataset_uuid' 8433 : 'zone_dataset_uuid'); 8434 8435 if (payload.hasOwnProperty('image_uuid')) { 8436 log.warn('DEPRECATED option ' + property + ' found, ' 8437 + 'ignoring. In the future use image_uuid only.'); 8438 } else { 8439 log.warn('DEPRECATED option ' + property + ' found, ' 8440 + 'ignoring. In the future use image_uuid instead.'); 8441 payload.image_uuid = payload[property]; 8442 delete payload.dataset_uuid; 8443 } 8444 } 8445 8446 // after ZoneDefaults have been applied, we should always have zone. Now 8447 // we validate the payload properties and remove any that are invalid. If 8448 // there are bad values we'll just fail. 8449 VM.validate(brand, action, payload, {log: log}, function (errors) { 8450 var bad_prop; 8451 var compound_props = ['disks', 'nics', 'filesystems']; 8452 var matches; 8453 var obj; 8454 var prop; 8455 8456 if (errors) { 8457 if (errors.hasOwnProperty('bad_brand')) { 8458 callback(new Error('Invalid brand while validating payload: ' 8459 + JSON.stringify(brand))); 8460 return; 8461 } 8462 if (errors.bad_values.length > 0) { 8463 callback(new Error('Invalid value(s) for: ' 8464 + errors.bad_values.join(','))); 8465 return; 8466 } 8467 if (errors.missing_properties.length > 0) { 8468 callback(new Error('Missing required properties: ' 8469 + errors.missing_properties.join(','))); 8470 return; 8471 } 8472 for (bad_prop in errors.bad_properties) { 8473 bad_prop = errors.bad_properties[bad_prop]; 8474 log.warn('Warning, invalid ' + action + ' property: [' 8475 + bad_prop + '] removing from payload.'); 8476 8477 // for bad properties like nics.*.allow_unfiltered_promisc we 8478 // need to remove it from add_nics, update_nics, etc. 8479 for (prop in compound_props) { 8480 prop = compound_props[prop]; 8481 8482 matches = new RegExp('^' + prop 8483 + '\\.\\*\\.(.*)$').exec(bad_prop); 8484 if (matches) { 8485 if (payload.hasOwnProperty(prop)) { 8486 for (obj in payload[prop]) { 8487 delete payload[prop][obj][matches[1]]; 8488 } 8489 } 8490 if (payload.hasOwnProperty('add_' + prop)) { 8491 for (obj in payload['add_' + prop]) { 8492 delete payload['add_' + prop][obj][matches[1]]; 8493 } 8494 } 8495 if (payload.hasOwnProperty('update_' + prop)) { 8496 for (obj in payload['update_' + prop]) { 8497 delete payload['update_' 8498 + prop][obj][matches[1]]; 8499 } 8500 } 8501 } 8502 } 8503 8504 delete payload[bad_prop]; 8505 } 8506 } 8507 8508 // By the time we got here all the properties in the payload are allowed 8509 8510 // Now we make sure we've got a zonename (use uuid if not already set) 8511 if (!payload.hasOwnProperty('zonename') 8512 || payload.zonename === undefined) { 8513 8514 payload.zonename = payload.uuid; 8515 } 8516 8517 // You use 'disks' and 'nics' when creating, but the underlying 8518 // functions expect add_disks and add_nics, so we rename them now that 8519 // we've confirmed we've got the correct thing for this action. 8520 if (payload.hasOwnProperty('disks')) { 8521 if (payload.hasOwnProperty('add_disks')) { 8522 callback(new Error('Cannot specify both "disks" and ' 8523 + '"add_disks"')); 8524 return; 8525 } 8526 payload.add_disks = payload.disks; 8527 delete payload.disks; 8528 } 8529 if (payload.hasOwnProperty('nics')) { 8530 if (payload.hasOwnProperty('add_nics')) { 8531 callback(new Error('Cannot specify both "nics" and ' 8532 + '"add_nics"')); 8533 return; 8534 } 8535 payload.add_nics = payload.nics; 8536 delete payload.nics; 8537 } 8538 if (payload.hasOwnProperty('filesystems')) { 8539 if (payload.hasOwnProperty('add_filesystems')) { 8540 callback(new Error('Cannot specify both "filesystems" and ' 8541 + '"add_filesystems"')); 8542 return; 8543 } 8544 payload.add_filesystems = payload.filesystems; 8545 delete payload.filesystems; 8546 } 8547 8548 // if there's a zfs_root_* and no zfs_data_*, normally the properties 8549 // would fall through, we don't want that. 8550 if (payload.hasOwnProperty('zfs_root_compression') 8551 && !payload.hasOwnProperty('zfs_data_compression')) { 8552 8553 if (vmobj && vmobj.hasOwnProperty('zfs_data_compression')) { 8554 // keep existing value. 8555 payload.zfs_data_compression = vmobj.zfs_data_compression; 8556 } else { 8557 // keep default value. 8558 payload.zfs_data_compression = 'off'; 8559 } 8560 } 8561 if (payload.hasOwnProperty('zfs_root_recsize') 8562 && !payload.hasOwnProperty('zfs_data_recsize')) { 8563 8564 if (vmobj && vmobj.hasOwnProperty('zfs_data_recsize')) { 8565 // keep existing value. 8566 payload.zfs_data_recsize = vmobj.zfs_data_recsize; 8567 } else { 8568 // keep default value. 8569 payload.zfs_data_recsize = 131072; 8570 } 8571 } 8572 8573 // this will ensure we've got a MAC, etc. 8574 normalizeNics(payload, vmobj); 8575 8576 // Fix types for boolean fields in case someone put in 'false'/'true' 8577 // instead of false/true 8578 for (property in payload) { 8579 if (payload.hasOwnProperty(property)) { 8580 if (PAYLOAD_PROPERTIES.hasOwnProperty(property) 8581 && PAYLOAD_PROPERTIES[property].type === 'boolean') { 8582 8583 payload[property] = fixBooleanLoose(payload[property]); 8584 } 8585 } 8586 } 8587 8588 // We used to support zfs_storage_pool_name, but zpool is better. 8589 if (payload.hasOwnProperty('zfs_storage_pool_name')) { 8590 if (payload.hasOwnProperty('zpool')) { 8591 log.warn('DEPRECATED option zfs_storage_pool_name found, ' 8592 + 'ignoring!'); 8593 } else { 8594 log.warn('DEPRECATED option zfs_storage_pool_name found, ' 8595 + 'replacing with zpool!'); 8596 payload.zpool = payload.zfs_storage_pool_name; 8597 delete payload.zfs_storage_pool_name; 8598 } 8599 } 8600 8601 // When creating a VM with SPICE you need the image_uuid, if you don't 8602 // pass that, we'll remove any SPICE options. 8603 if (action === 'create' 8604 && !payload.hasOwnProperty('image_uuid')) { 8605 8606 if (payload.hasOwnProperty('spice_opts') 8607 || payload.hasOwnProperty('spice_password') 8608 || payload.hasOwnProperty('spice_port')) { 8609 8610 log.warn('Creating with SPICE options requires ' 8611 + 'image_uuid, REMOVING spice_*'); 8612 delete payload.spice_opts; 8613 delete payload.spice_password; 8614 delete payload.spice_port; 8615 } 8616 } 8617 8618 checkPayloadProperties(payload, vmobj, log, function (e) { 8619 if (e) { 8620 callback(e); 8621 } else { 8622 callback(); 8623 } 8624 }); 8625 }); 8626 } 8627 8628 function buildTransitionZonecfg(transition, target, timeout) 8629 { 8630 var cmdline; 8631 8632 cmdline = 'add attr; set name=transition; set value="' 8633 + transition + ':' + target + ':' + (Date.now(0) + timeout).toString() 8634 + '"; set type=string; end'; 8635 8636 return cmdline; 8637 } 8638 8639 // vmobj should have: 8640 // 8641 // uuid 8642 // transition_to (if set) 8643 // 8644 exports.unsetTransition = function (vmobj, options, callback) 8645 { 8646 var log; 8647 8648 // options is optional 8649 if (arguments.length === 2) { 8650 callback = arguments[1]; 8651 options = {}; 8652 } 8653 8654 ensureLogging(true); 8655 if (options.hasOwnProperty('log')) { 8656 log = options.log; 8657 } else { 8658 log = VM.log.child({action: 'unsetTransition', vm: vmobj.uuid}); 8659 } 8660 8661 zonecfg(['-u', vmobj.uuid, 'remove -F attr name=transition'], log, 8662 function (err, fds) { 8663 8664 if (err) { 8665 // log at info because this might be because already removed 8666 log.info({err: err, stdout: fds.stdout, stderr: fds.stderr}, 8667 'unable to remove transition for zone ' + vmobj.uuid); 8668 } else { 8669 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8670 'removed transition for zone ' + vmobj.uuid); 8671 } 8672 8673 zonecfg(['-u', vmobj.uuid, 'info attr name=transition'], log, 8674 function (info_err, info_fds) { 8675 8676 if (info_err) { 8677 log.error({err: info_err, stdout: info_fds.stdout, 8678 stderr: info_fds.stderr}, 8679 'failed to confirm transition removal'); 8680 callback(info_err); 8681 return; 8682 } 8683 8684 if (info_fds.stdout !== 'No such attr resource.\n') { 8685 log.error({stdout: info_fds.stdout, stderr: info_fds.stderr}, 8686 'unknown error checking transition after removal'); 8687 callback(new Error('transition does not appear to have been ' 8688 + 'removed zonecfg said: ' + JSON.stringify(info_fds))); 8689 return; 8690 } 8691 8692 // removed the transition, now attempt to start if we're rebooting. 8693 if (vmobj.transition_to && vmobj.transition_to === 'start') { 8694 log.debug('VM ' + vmobj.uuid + ' was stopping for reboot, ' 8695 + 'transitioning to start.'); 8696 VM.start(vmobj.uuid, {}, {log: log}, function (e) { 8697 if (e) { 8698 log.error(e, 'failed to start when clearing ' 8699 + 'transition'); 8700 } 8701 callback(); 8702 }); 8703 } else { 8704 callback(); 8705 } 8706 }); 8707 }); 8708 }; 8709 8710 // 8711 // vmobj fields used: 8712 // 8713 // transition 8714 // uuid 8715 // 8716 function setTransition(vmobj, transition, target, timeout, log, callback) 8717 { 8718 assert(log, 'no logger passed to setTransition()'); 8719 8720 if (!timeout) { 8721 callback(new Error('setTransition() requires timeout argument.')); 8722 return; 8723 } 8724 8725 async.series([ 8726 function (cb) { 8727 // unset an existing transition 8728 if (vmobj.hasOwnProperty('transition')) { 8729 VM.unsetTransition(vmobj, {log: log}, cb); 8730 } else { 8731 cb(); 8732 } 8733 }, function (cb) { 8734 var zcfg; 8735 8736 zcfg = buildTransitionZonecfg(transition, target, timeout); 8737 zonecfg(['-u', vmobj.uuid, zcfg], log, function (err, fds) { 8738 if (err) { 8739 log.error({err: err, stdout: fds.stdout, 8740 stderr: fds.stderr}, 'failed to set transition=' 8741 + transition + ' for VM ' + vmobj.uuid); 8742 } else { 8743 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 8744 'set transition=' + transition + ' for vm ' 8745 + vmobj.uuid); 8746 } 8747 8748 cb(err); 8749 }); 8750 } 8751 ], function (error) { 8752 callback(error); 8753 }); 8754 } 8755 8756 function receiveVM(json, log, callback) 8757 { 8758 var payload = {}; 8759 8760 assert(log, 'no logger passed to receiveVM()'); 8761 8762 try { 8763 payload = JSON.parse(json); 8764 } catch (e) { 8765 callback(e); 8766 return; 8767 } 8768 8769 payload.create_only = true; 8770 8771 // adding transition here is considered to be *internal only* not for 8772 // consumer use and not to be documented as a property you can use with 8773 // create. 8774 payload.transition = 8775 {'transition': 'receiving', 'target': 'stopped', 'timeout': 86400}; 8776 8777 // We delete tags and metadata here becasue this exists in the root 8778 // dataset which we will be copying, so it would be duplicated here. 8779 delete payload.customer_metadata; 8780 delete payload.internal_metadata; 8781 delete payload.tags; 8782 8783 // On receive we need to make sure that we don't create new disks so we 8784 // mark them all as nocreate. We also can't set the block_size of imported 8785 // volumes, so we remove that. 8786 if (payload.hasOwnProperty('disks')) { 8787 var disk_idx; 8788 8789 for (disk_idx in payload.disks) { 8790 payload.disks[disk_idx].nocreate = true; 8791 8792 if (payload.disks[disk_idx].image_uuid) { 8793 delete payload.disks[disk_idx].block_size; 8794 } 8795 } 8796 } 8797 8798 VM.create(payload, {log: log}, function (err, result) { 8799 if (err) { 8800 callback(err); 8801 } 8802 8803 // don't include the special transition in the payload we write out. 8804 delete payload.transition; 8805 8806 fs.writeFile('/etc/zones/' + payload.uuid + '-receiving.json', 8807 JSON.stringify(payload, null, 2), function (e) { 8808 8809 if (e) { 8810 callback(e); 8811 return; 8812 } 8813 8814 // ready for datasets 8815 callback(null, result); 8816 }); 8817 }); 8818 } 8819 8820 function receiveStdinChunk(type, log, callback) 8821 { 8822 var child; 8823 var chunk_name = ''; 8824 var chunk_size = 0; 8825 var json = ''; 8826 var remaining = ''; 8827 8828 assert(log, 'no logger passed to receiveStdinChunk()'); 8829 8830 /* 8831 * XXX 8832 * 8833 * node 0.6.x removed support for arbitrary file descriptors which 8834 * means we can only handle stdin for now since we need to pass this 8835 * descriptor directly to the child. 0.8.x is supposed to reintroduce 8836 * this functionality. When we do, this should be changed to open 8837 * the file and set fd to the descriptor, and we should be able to 8838 * get rid of vmunbundle. 8839 * 8840 */ 8841 8842 if (type === 'JSON') { 8843 log.info('/usr/vm/sbin/vmunbundle json'); 8844 child = spawn('/usr/vm/sbin/vmunbundle', ['json'], 8845 {customFds: [0, -1, -1]}); 8846 } else if (type === 'DATASET') { 8847 log.info('/usr/vm/sbin/vmunbundle dataset'); 8848 child = spawn('/usr/vm/sbin/vmunbundle', ['dataset'], 8849 {customFds: [0, -1, -1]}); 8850 } else { 8851 callback(new Error('Unsupported chunk type ' + type)); 8852 } 8853 8854 child.stderr.on('data', function (data) { 8855 var idx; 8856 var line; 8857 var matches; 8858 8859 remaining += data.toString(); 8860 8861 idx = remaining.indexOf('\n'); 8862 while (idx > -1) { 8863 line = trim(remaining.substring(0, idx)); 8864 remaining = remaining.substring(idx + 1); 8865 8866 log.debug('VMUNBUNDLE: ' + line); 8867 matches = line.match(/Size: ([\d]+)/); 8868 if (matches) { 8869 chunk_size = Number(matches[1]); 8870 } 8871 matches = line.match(/Name: \[(.*)\]/); 8872 if (matches) { 8873 chunk_name = matches[1]; 8874 } 8875 8876 idx = remaining.indexOf('\n'); 8877 } 8878 }); 8879 8880 child.stdout.on('data', function (data) { 8881 json += data.toString(); 8882 log.debug('json size is ' + json.length); 8883 }); 8884 8885 child.on('close', function (code) { 8886 log.debug('vmunbundle process exited with code ' + code); 8887 if (code === 3) { 8888 log.debug('vmbundle: end of bundle.'); 8889 callback(null, 'EOF'); 8890 return; 8891 } else if (code !== 0) { 8892 callback(new Error('vmunbundle exited with code ' + code)); 8893 return; 8894 } 8895 8896 // if it was a dataset, we've now imported it. 8897 // if it was json, we've now got it in the json var. 8898 8899 if (type === 'DATASET') { 8900 log.info('Imported dataset ' + chunk_name); 8901 // delete 'sending' snapshot 8902 zfs(['destroy', '-F', chunk_name + '@sending'], log, 8903 function (err, fds) { 8904 if (err) { 8905 log.warn(err, 'Failed to destroy ' + chunk_name 8906 + '@sending: ' + err.message); 8907 } 8908 callback(); 8909 } 8910 ); 8911 } else if (type === 'JSON' && chunk_name === 'JSON' 8912 && json.length <= chunk_size && json.length > 0) { 8913 8914 receiveVM(json, log, function (e, result) { 8915 if (e) { 8916 callback(e); 8917 return; 8918 } 8919 log.info('Receive returning: ' + JSON.stringify(result)); 8920 callback(null, result); 8921 }); 8922 } else { 8923 log.debug('type: [' + type + ']'); 8924 log.debug('chunk_name: [' + chunk_name + ']'); 8925 log.debug('chunk_size: [' + chunk_size + ']'); 8926 log.debug('json.length: [' + json.length + ']'); 8927 log.warn('Failed to get ' + type + '!'); 8928 callback(new Error('Failed to get ' + type + '!')); 8929 } 8930 }); 8931 } 8932 8933 exports.receive = function (target, options, callback) 8934 { 8935 var log; 8936 8937 // options is optional 8938 if (arguments.length === 2) { 8939 callback = arguments[1]; 8940 options = {}; 8941 } 8942 8943 ensureLogging(true); 8944 8945 // We don't know anything about this VM yet, so we don't create a 8946 // VM.log.child. 8947 if (options.hasOwnProperty('log')) { 8948 log = options.log; 8949 } else { 8950 log = VM.log; 8951 } 8952 8953 log.info('Receiving VM from: ' + JSON.stringify(target)); 8954 8955 if (target.hasOwnProperty('host') && target.hasOwnProperty('port')) { 8956 // network receive not yet supported either. 8957 callback(new Error('cannot receive from ' + JSON.stringify(target))); 8958 return; 8959 } else if (typeof (target) !== 'string' || target !== '-') { 8960 callback(new Error('cannot receive from ' + JSON.stringify(target))); 8961 return; 8962 } 8963 8964 receiveStdinChunk('JSON', log, function (error, result) { 8965 var eof = false; 8966 8967 if (error) { 8968 callback(error); 8969 return; 8970 } 8971 if (result && result === 'EOF') { 8972 callback(new Error('unable to find JSON in stdin.')); 8973 } else if (result && result.hasOwnProperty('uuid')) { 8974 // VM started receive, now need datasets 8975 8976 // We have JSON, so we can log better now if we need one 8977 if (!options.hasOwnProperty('log')) { 8978 log = VM.log.child({action: 'receive', vm: result.uuid}); 8979 } 8980 8981 log.info('Receiving VM ' + result.uuid); 8982 log.debug('now looking for datasets'); 8983 8984 async.whilst( 8985 function () { return !eof; }, 8986 function (cb) { 8987 receiveStdinChunk('DATASET', log, function (err, res) { 8988 if (err) { 8989 cb(err); 8990 return; 8991 } 8992 if (res === 'EOF') { 8993 eof = true; 8994 } 8995 cb(); 8996 }); 8997 }, function (err) { 8998 if (err) { 8999 callback(err); 9000 return; 9001 } 9002 // no error so we read all the datasets, try an install. 9003 log.info('receive calling VM.install: ' + eof); 9004 VM.install(result.uuid, {log: log}, function (e) { 9005 if (e) { 9006 log.warn(e, 'couldn\'t install VM: ' 9007 + e.message); 9008 } 9009 callback(e, result); 9010 }); 9011 } 9012 ); 9013 } else { 9014 callback(new Error('unable to receive JSON')); 9015 } 9016 }); 9017 }; 9018 9019 exports.reprovision = function (uuid, payload, options, callback) 9020 { 9021 var log; 9022 var provision_timeout = PROVISION_TIMEOUT; 9023 var set_transition = false; 9024 var snapshot; 9025 var vmobj; 9026 9027 // options is optional 9028 if (arguments.length === 3) { 9029 callback = arguments[2]; 9030 options = {}; 9031 } 9032 9033 ensureLogging(true); 9034 if (options.hasOwnProperty('log')) { 9035 log = options.log; 9036 } else { 9037 log = VM.log.child({action: 'reprovision', vm: uuid}); 9038 } 9039 9040 log.info('Reprovisioning VM ' + uuid + ', original payload:\n' 9041 + JSON.stringify(payload, null, 2)); 9042 9043 async.waterfall([ 9044 function (cb) { 9045 VM.load(uuid, { 9046 fields: [ 9047 'brand', 9048 'datasets', 9049 'hostname', 9050 'nics', 9051 'quota', 9052 'state', 9053 'uuid', 9054 'zfs_filesystem', 9055 'zone_state', 9056 'zonename', 9057 'zonepath', 9058 'zpool' 9059 ], 9060 log: log 9061 }, function (err, obj) { 9062 if (err) { 9063 cb(err); 9064 return; 9065 } 9066 vmobj = obj; 9067 log.debug('Loaded VM is: ' + JSON.stringify(vmobj, null, 2)); 9068 cb(); 9069 }); 9070 }, function (cb) { 9071 if (BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 9072 && BRAND_OPTIONS[vmobj.brand].features.reprovision 9073 && BRAND_OPTIONS[vmobj.brand].features.brand_install_script) { 9074 9075 cb(); 9076 } else { 9077 cb(new Error('brand "' + vmobj.brand + '" does not yet support' 9078 + ' reprovision')); 9079 } 9080 }, function (cb) { 9081 // only support image_uuid at top level (for non-KVM currently) 9082 if (!payload.hasOwnProperty('image_uuid')) { 9083 cb(new Error('payload is missing image_uuid')); 9084 } else { 9085 cb(); 9086 } 9087 }, function (cb) { 9088 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 1) { 9089 cb(new Error('cannot support reprovision with multiple ' 9090 + 'delegated datasets')); 9091 return; 9092 } else if (vmobj.hasOwnProperty('datasets') 9093 && vmobj.datasets.length === 1 9094 && vmobj.datasets[0] !== vmobj.zfs_filesystem + '/data') { 9095 9096 cb(new Error('cannot support reprovision with non-standard "' 9097 + vmobj.datasets[0] + '" dataset')); 9098 return; 9099 } 9100 cb(); 9101 }, function (cb) { 9102 // TODO: change here when we support zvols/KVM, add size 9103 // & change type 9104 9105 validateImage({ 9106 type: 'zone-dataset', 9107 uuid: payload.image_uuid, 9108 zpool: vmobj.zpool 9109 }, log, function (e) { 9110 cb(e); 9111 }); 9112 }, function (cb) { 9113 // ensure we're stopped before reprovision starts 9114 if (vmobj.zone_state !== 'installed') { 9115 VM.stop(uuid, {log: log}, function (e) { 9116 if (e) { 9117 log.error(e, 'unable to stop VM ' + uuid + ': ' 9118 + e.message); 9119 } 9120 cb(e); 9121 }); 9122 } else { 9123 cb(); 9124 } 9125 }, function (cb) { 9126 // Set transition to provisioning now, we're going for it. 9127 setTransition(vmobj, 'provisioning', 'running', 9128 (provision_timeout * 1000), log, function (err) { 9129 if (err) { 9130 cb(err); 9131 } else { 9132 set_transition = true; 9133 cb(); 9134 } 9135 }); 9136 }, function (cb) { 9137 // we validated any delegated dataset above, so we just need to 9138 // remove the 'zoned' flag if we've got one. 9139 if (!vmobj.hasOwnProperty('datasets') 9140 || vmobj.datasets.length === 0) { 9141 9142 cb(); 9143 return; 9144 } 9145 zfs(['set', 'zoned=off', vmobj.datasets[0]], log, 9146 function (err, fds) { 9147 9148 if (err) { 9149 log.error({err: err, stdout: fds.stdout, 9150 stderr: fds.stderr}, 'Unable to turn off "zoned" for ' 9151 + vmobj.datasets[0]); 9152 } 9153 cb(err); 9154 }); 9155 }, function (cb) { 9156 // if we have a delegated dataset, rename zones/<uuid>/data 9157 // -> zones/<uuid>-reprovisioning-data 9158 if (!vmobj.hasOwnProperty('datasets') 9159 || vmobj.datasets.length === 0) { 9160 9161 cb(); 9162 return; 9163 } 9164 zfs(['rename', '-f', vmobj.datasets[0], vmobj.zfs_filesystem 9165 + '-reprovisioning-data'], log, function (err, fds) { 9166 9167 if (err) { 9168 log.error({err: err, stdout: fds.stdout, 9169 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9170 + vmobj.datasets[0]); 9171 } 9172 cb(err); 9173 }); 9174 }, function (cb) { 9175 // unmount <zonepath>/cores so dataset is not busy 9176 zfs(['umount', vmobj.zonepath + '/cores'], log, 9177 function (err, fds) { 9178 9179 if (err) { 9180 if (trim(fds.stderr).match(/not a mountpoint$/)) { 9181 log.info('ignoring failure to umount cores which ' 9182 + 'wasn\'t mounted'); 9183 cb(); 9184 return; 9185 } else { 9186 log.error({err: err, stdout: fds.stdout, 9187 stderr: fds.stderr}, 'Unable to umount ' 9188 + vmobj.zonepath + '/cores'); 9189 } 9190 } 9191 cb(err); 9192 }); 9193 }, function (cb) { 9194 // rename <zfs_filesystem> dataset out of the way 9195 zfs(['rename', '-f', vmobj.zfs_filesystem, vmobj.zfs_filesystem 9196 + '-reprovisioning-root'], log, function (err, fds) { 9197 9198 if (err) { 9199 log.error({err: err, stdout: fds.stdout, 9200 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9201 + vmobj.zfs_filesystem); 9202 } 9203 cb(err); 9204 }); 9205 }, function (cb) { 9206 var snapname = vmobj.zpool + '/' + payload.image_uuid + '@final'; 9207 9208 // ensure we've got our snapshot 9209 zfs(['get', '-Ho', 'value', 'type', snapname], log, 9210 function (err, fds) { 9211 9212 if (!err) { 9213 // snapshot already exists, use it 9214 log.debug('snapshot "' + snapname + '" exists'); 9215 snapshot = snapname; 9216 cb(); 9217 return; 9218 } 9219 9220 if (fds.stderr.match(/dataset does not exist/)) { 9221 // we'll use a different one. (falls throught to next func) 9222 cb(); 9223 } else { 9224 cb(err); 9225 } 9226 }); 9227 }, function (cb) { 9228 var snapname; 9229 9230 if (snapshot) { 9231 // already know which one to use, don't create one 9232 cb(); 9233 return; 9234 } 9235 9236 snapname = vmobj.zpool + '/' + payload.image_uuid 9237 + '@' + vmobj.uuid; 9238 9239 // ensure we've got a snapshot 9240 zfs(['get', '-Ho', 'value', 'type', snapname], log, 9241 function (err, fds) { 9242 9243 if (!err) { 9244 // snapshot already exists, use it 9245 log.debug('snapshot "' + snapname + '" exists'); 9246 snapshot = snapname; 9247 cb(); 9248 return; 9249 } 9250 9251 if (fds.stderr.match(/dataset does not exist/)) { 9252 zfs(['snapshot', snapname], log, function (e, snap_fds) { 9253 if (e) { 9254 e.stdout = snap_fds.stdout; 9255 e.stderr = snap_fds.stderr; 9256 log.error(e, 'Failed to create snapshot: ' 9257 + e.message); 9258 } else { 9259 log.debug('created snapshot "' + snapname + '"'); 9260 snapshot = snapname; 9261 } 9262 cb(e); 9263 }); 9264 } else { 9265 cb(err); 9266 return; 9267 } 9268 }); 9269 }, function (cb) { 9270 var args; 9271 9272 // clone the new image creating a new dataset for zoneroot 9273 assert(snapshot); 9274 9275 args = ['clone']; 9276 if (vmobj.hasOwnProperty('quota') && vmobj.quota > 0) { 9277 args.push('-o'); 9278 args.push('quota=' + vmobj.quota + 'G'); 9279 } 9280 args.push(snapshot); 9281 args.push(vmobj.zfs_filesystem); 9282 9283 zfs(args, log, function (err, fds) { 9284 if (err) { 9285 log.error({err: err, stdout: fds.stdout, 9286 stderr: fds.stderr}, 'Unable to create new clone of ' 9287 + payload.image_uuid); 9288 } 9289 cb(err); 9290 }); 9291 }, function (cb) { 9292 var cmd; 9293 9294 // copy zones/<uuid>-reprovisioning-root/config to 9295 // zones/<uuid>/config so we keep metadata and ipf rules. 9296 try { 9297 fs.mkdirSync(vmobj.zonepath + '/config'); 9298 } catch (e) { 9299 if (e.code !== 'EEXIST') { 9300 e.message = 'Unable to recreate ' + vmobj.zonepath 9301 + '/config: ' + e.message; 9302 cb(e); 9303 return; 9304 } 9305 } 9306 9307 cmd = 'cp -pPR ' 9308 + vmobj.zonepath + '-reprovisioning-root/config/* ' 9309 + vmobj.zonepath + '/config/'; 9310 9311 log.debug(cmd); 9312 exec(cmd, function (error, stdout, stderr) { 9313 log.debug({'stdout': stdout, 'stderr': stderr}, 'cp results'); 9314 if (error) { 9315 error.stdout = stdout; 9316 error.stderr = stderr; 9317 cb(error); 9318 return; 9319 } else { 9320 cb(); 9321 } 9322 }); 9323 }, function (cb) { 9324 // destroy <zonepath>-reprovisioning-root, since it's no longer used 9325 zfs(['destroy', '-r', vmobj.zfs_filesystem 9326 + '-reprovisioning-root'], log, function (err, fds) { 9327 9328 if (err) { 9329 log.error({err: err, stdout: fds.stdout, 9330 stderr: fds.stderr}, 'Unable to destroy ' 9331 + vmobj.zfs_filesystem + '-reprovisioning-root: ' 9332 + err.message); 9333 } 9334 cb(err); 9335 }); 9336 }, function (cb) { 9337 // remount /zones/<uuid>/cores 9338 zfs(['mount', vmobj.zpool + '/cores/' + uuid], log, 9339 function (err, fds) { 9340 9341 if (err) { 9342 log.error({err: err, stdout: fds.stdout, 9343 stderr: fds.stderr}, 'Unable to mount ' + vmobj.zonepath 9344 + '/cores: ' + err.message); 9345 } 9346 cb(err); 9347 }); 9348 }, function (cb) { 9349 var args = ['-r', '-R', vmobj.zonepath, '-z', vmobj.zonename]; 9350 var cmd = BRAND_OPTIONS[vmobj.brand].features.brand_install_script; 9351 9352 // We run the brand's install script here with the -r flag which 9353 // tells it to do everything that's relevant to reprovision. 9354 9355 log.debug(cmd + ' ' + args.join(' ')); 9356 execFile(cmd, args, function (error, stdout, stderr) { 9357 var new_err; 9358 9359 if (error) { 9360 new_err = new Error('Error running brand install script ' 9361 + cmd); 9362 // error's message includes stderr. 9363 log.error({err: error, stdout: stdout}, 9364 'brand install script exited with code ' + error.code); 9365 cb(new_err); 9366 } else { 9367 log.debug(cmd + ' stderr:\n' + stderr); 9368 cb(); 9369 } 9370 }); 9371 }, function (cb) { 9372 // rename zones/<uuid>-reprovision-data -> zones/<uuid>/data 9373 if (!vmobj.hasOwnProperty('datasets') 9374 || vmobj.datasets.length === 0) { 9375 9376 cb(); 9377 return; 9378 } 9379 zfs(['rename', '-f', vmobj.zfs_filesystem + '-reprovisioning-data', 9380 vmobj.datasets[0]], log, function (err, fds) { 9381 9382 if (err) { 9383 log.error({err: err, stdout: fds.stdout, 9384 stderr: fds.stderr}, 'Unable to (temporarily) rename ' 9385 + vmobj.zfs_filesystem); 9386 } 9387 cb(err); 9388 }); 9389 }, function (cb) { 9390 // set zoned=on for zones/<uuid>/data 9391 if (!vmobj.hasOwnProperty('datasets') 9392 || vmobj.datasets.length === 0) { 9393 9394 cb(); 9395 return; 9396 } 9397 zfs(['set', 'zoned=on', vmobj.datasets[0]], log, 9398 function (err, fds) { 9399 9400 if (err) { 9401 log.error({err: err, stdout: fds.stdout, 9402 stderr: fds.stderr}, 'Unable to set "zoned" for: ' 9403 + vmobj.datasets[0]); 9404 } 9405 cb(err); 9406 }); 9407 }, function (cb) { 9408 // update zone's image_uuid field 9409 var zcfg = 'select attr name=dataset-uuid; set value="' 9410 + payload.image_uuid + '"; end'; 9411 zonecfg(['-u', uuid, zcfg], log, function (err, fds) { 9412 if (err) { 9413 log.error({err: err, stdout: fds.stdout, 9414 stderr: fds.stderr}, 'unable to set image_uuid on VM ' 9415 + uuid); 9416 } 9417 cb(err); 9418 }); 9419 }, function (cb) { 9420 var p = { 9421 autoboot: true, 9422 reprovisioning: true, 9423 uuid: uuid, 9424 zonename: vmobj.zonename, 9425 zonepath: vmobj.zonepath 9426 }; 9427 9428 // NOTE: someday we could allow mdata_exec_timeout in the original 9429 // payload to reprovision and then pass it along here. 9430 9431 // other fields used by installZone() 9432 [ 9433 'dns_domain', 9434 'hostname', 9435 'quota', 9436 'resolvers', 9437 'tmpfs', 9438 'zfs_filesystem', 9439 'zfs_root_compression', 9440 'zfs_root_recsize' 9441 ].forEach(function (k) { 9442 if (vmobj.hasOwnProperty(k)) { 9443 p[k] = vmobj[k]; 9444 } 9445 }); 9446 9447 // nics needs to be called add_nics here 9448 if (vmobj.hasOwnProperty('nics')) { 9449 p.add_nics = vmobj.nics; 9450 } 9451 9452 installZone(p, log, function (err) { 9453 log.debug(err, 'ran installZone() for reprovision'); 9454 cb(err); 9455 }); 9456 } 9457 ], function (err) { 9458 if (err && set_transition) { 9459 // remove transition now, if we failed. 9460 VM.unsetTransition(vmobj, {log: log}, function () { 9461 // err here is original err, we ignore failure to unset because 9462 // nothing we can do about that.. 9463 callback(err); 9464 }); 9465 } else { 9466 callback(err); 9467 } 9468 }); 9469 }; 9470 9471 exports.install = function (uuid, options, callback) 9472 { 9473 var log; 9474 9475 // options is optional 9476 if (arguments.length === 2) { 9477 callback = arguments[1]; 9478 options = {}; 9479 } 9480 9481 ensureLogging(true); 9482 if (options.hasOwnProperty('log')) { 9483 log = options.log; 9484 } else { 9485 log = VM.log.child({action: 'install', vm: uuid}); 9486 } 9487 9488 log.info('Installing VM ' + uuid); 9489 9490 fs.readFile('/etc/zones/' + uuid + '-receiving.json', 9491 function (err, data) { 9492 var payload; 9493 9494 if (err) { 9495 callback(err); 9496 return; 9497 } 9498 9499 try { 9500 payload = JSON.parse(data.toString()); 9501 } catch (e) { 9502 callback(e); 9503 return; 9504 } 9505 9506 // installZone takes a payload 9507 installZone(payload, log, callback); 9508 } 9509 ); 9510 9511 }; 9512 9513 function getAllDatasets(vmobj) 9514 { 9515 var datasets = []; 9516 var disk; 9517 9518 if (vmobj.hasOwnProperty('zfs_filesystem')) { 9519 datasets.push(vmobj.zfs_filesystem); 9520 } 9521 9522 for (disk in vmobj.disks) { 9523 disk = vmobj.disks[disk]; 9524 if (disk.hasOwnProperty('zfs_filesystem')) { 9525 datasets.push(disk.zfs_filesystem); 9526 } 9527 } 9528 9529 return datasets; 9530 } 9531 9532 // 9533 // Headers are 512 bytes and look like: 9534 // 9535 // MAGIC-VMBUNDLE\0 9536 // <VERSION>\0 -- ASCII #s 9537 // <CHECKSUM>\0 -- ASCII (not yet used) 9538 // <OBJ-NAME>\0 -- max length: 256 9539 // <OBJ-SIZE>\0 -- ASCII # of bytes 9540 // <PADDED-SIZE>\0 -- ASCII # of bytes, must be multiple of 512 9541 // ...\0 9542 // 9543 function chunkHeader(name, size, padding) 9544 { 9545 var header = new Buffer(512); 9546 var pos = 0; 9547 9548 header.fill(0); 9549 pos += addString(header, 'MAGIC-VMBUNDLE', pos); 9550 pos += addString(header, sprintf('%d', 1), pos); 9551 pos += addString(header, 'CHECKSUM', pos); 9552 pos += addString(header, name, pos); 9553 pos += addString(header, sprintf('%d', size), pos); 9554 pos += addString(header, sprintf('%d', size + padding), pos); 9555 9556 return (header); 9557 } 9558 9559 // add the string to buffer at pos, returning pos of new end of the buffer. 9560 function addString(buf, str, pos) 9561 { 9562 var len = str.length; 9563 buf.write(str, pos); 9564 return (len + 1); 9565 } 9566 9567 function sendJSON(target, json, log, cb) 9568 { 9569 var header; 9570 var pad; 9571 var padding = 0; 9572 9573 assert(log, 'no logger passed for sendJSON()'); 9574 9575 if (target === 'stdout') { 9576 if ((json.length % 512) != 0) { 9577 padding = 512 - (json.length % 512); 9578 } 9579 header = chunkHeader('JSON', json.length, padding); 9580 process.stdout.write(header); 9581 process.stdout.write(json, 'ascii'); 9582 if (padding > 0) { 9583 pad = new Buffer(padding); 9584 pad.fill(0); 9585 process.stdout.write(pad); 9586 } 9587 cb(); 9588 } else { 9589 log.error('Don\'t know how to send JSON to ' 9590 + JSON.stringify(target)); 9591 cb(new Error('Don\'t know how to send JSON to ' 9592 + JSON.stringify(target))); 9593 } 9594 } 9595 9596 function sendDataset(target, dataset, log, callback) 9597 { 9598 var header; 9599 9600 assert(log, 'no logger passed for sendDataset()'); 9601 9602 if (target === 'stdout') { 9603 9604 async.series([ 9605 function (cb) { 9606 // delete any existing 'sending' snapshot 9607 zfs(['destroy', '-F', dataset + '@sending'], log, 9608 function (err, fds) { 9609 // We don't expect this to succeed, since that means 9610 // something left an @sending around. Warn if succeeds. 9611 if (!err) { 9612 log.warn('Destroyed pre-existing ' + dataset 9613 + '@sending'); 9614 } 9615 cb(); 9616 } 9617 ); 9618 }, function (cb) { 9619 zfs(['snapshot', dataset + '@sending'], log, 9620 function (err, fds) { 9621 9622 cb(err); 9623 }); 9624 }, function (cb) { 9625 header = chunkHeader(dataset, 0, 0); 9626 process.stdout.write(header); 9627 cb(); 9628 }, function (cb) { 9629 var child; 9630 9631 child = spawn('/usr/sbin/zfs', 9632 ['send', '-p', dataset + '@sending'], 9633 {customFds: [-1, 1, -1]}); 9634 child.stderr.on('data', function (data) { 9635 var idx; 9636 var lines = trim(data.toString()).split('\n'); 9637 9638 for (idx in lines) { 9639 log.debug('zfs send: ' + trim(lines[idx])); 9640 } 9641 }); 9642 child.on('close', function (code) { 9643 log.debug('zfs send process exited with code ' 9644 + code); 9645 cb(); 9646 }); 9647 }, function (cb) { 9648 zfs(['destroy', '-F', dataset + '@sending'], log, 9649 function (err, fds) { 9650 if (err) { 9651 log.warn(err, 'Unable to destroy ' + dataset 9652 + '@sending: ' + err.message); 9653 } 9654 cb(err); 9655 } 9656 ); 9657 } 9658 ], function (err) { 9659 if (err) { 9660 log.error(err, 'Failed to send dataset: ' + err.message); 9661 } else { 9662 log.info('Successfully sent dataset'); 9663 } 9664 callback(err); 9665 }); 9666 } else { 9667 log.error('Don\'t know how to send datasets to ' 9668 + JSON.stringify(target)); 9669 callback(new Error('Don\'t know how to send datasets to ' 9670 + JSON.stringify(target))); 9671 } 9672 } 9673 9674 exports.send = function (uuid, target, options, callback) 9675 { 9676 var datasets; 9677 var log; 9678 var vmobj; 9679 9680 // options is optional 9681 if (arguments.length === 3) { 9682 callback = arguments[2]; 9683 options = {}; 9684 } 9685 9686 ensureLogging(true); 9687 if (options.hasOwnProperty('log')) { 9688 log = options.log; 9689 } else { 9690 log = VM.log.child({action: 'send', vm: uuid}); 9691 } 9692 9693 target = 'stdout'; 9694 9695 log.info('Sending VM ' + uuid + ' to: ' + JSON.stringify(target)); 9696 async.series([ 9697 function (cb) { 9698 // make sure we *can* send first, to avoid wasting cycles 9699 if (target === 'stdout' && tty.isatty(1)) { 9700 log.error('Cannot send VM to a TTY.'); 9701 cb(new Error('Cannot send VM to a TTY.')); 9702 } else { 9703 cb(); 9704 } 9705 }, function (cb) { 9706 // NOTE: for this load we always load all fields, because we need 9707 // to send them all to the target machine. 9708 VM.load(uuid, {log: log}, function (err, obj) { 9709 if (err) { 9710 cb(err); 9711 } else { 9712 vmobj = obj; 9713 cb(); 9714 } 9715 }); 9716 }, function (cb) { 9717 datasets = getAllDatasets(vmobj); 9718 if (datasets.length < 1) { 9719 log.error('Cannot send VM with no datasets.'); 9720 cb(new Error('VM has no datasets.')); 9721 } else { 9722 cb(); 9723 } 9724 }, function (cb) { 9725 if (vmobj.state !== 'stopped') { 9726 // In this case we need to stop it and make sure it stopped. 9727 VM.stop(uuid, {log: log}, function (e) { 9728 if (e) { 9729 log.error(e, 'unable to stop VM ' + uuid + ': ' 9730 + e.message); 9731 cb(e); 9732 return; 9733 } 9734 VM.load(uuid, {fields: ['zone_state', 'uuid'], log: log}, 9735 function (error, obj) { 9736 9737 if (error) { 9738 log.error(error, 'unable to reload VM ' + uuid 9739 + ': ' + error.message); 9740 return; 9741 } 9742 if (obj.zone_state !== 'installed') { 9743 log.error('after stop attempt, state is ' 9744 + obj.zone_state + ' != installed'); 9745 cb(new Error('state after stopping is ' 9746 + obj.zone_state + ' != installed')); 9747 return; 9748 } 9749 cb(); 9750 }); 9751 }); 9752 } else { 9753 // already stopped, good to go! 9754 cb(); 9755 } 9756 }, function (cb) { 9757 // Clean up trash left from broken datasets (see OS-388) 9758 try { 9759 fs.unlinkSync(vmobj.zonepath + '/SUNWattached.xml'); 9760 } catch (err) { 9761 // DO NOTHING, this file shouldn't have existed anyway. 9762 } 9763 try { 9764 fs.unlinkSync(vmobj.zonepath + '/SUNWdetached.xml'); 9765 } catch (err) { 9766 // DO NOTHING, this file shouldn't have existed anyway. 9767 } 9768 cb(); 9769 }, function (cb) { 9770 // send JSON 9771 var json = JSON.stringify(vmobj, null, 2) + '\n'; 9772 sendJSON(target, json, log, cb); 9773 }, function (cb) { 9774 // send datasets 9775 async.forEachSeries(datasets, function (ds, c) { 9776 sendDataset(target, ds, log, c); 9777 }, function (e) { 9778 if (e) { 9779 log.error('Failed to send datasets'); 9780 } 9781 cb(e); 9782 }); 9783 } 9784 ], function (err) { 9785 callback(err); 9786 }); 9787 }; 9788 9789 exports.create = function (payload, options, callback) 9790 { 9791 var log; 9792 9793 // options is optional 9794 if (arguments.length === 2) { 9795 callback = arguments[1]; 9796 options = {}; 9797 } 9798 9799 ensureLogging(true); 9800 9801 if (options.hasOwnProperty('log')) { 9802 log = options.log; 9803 } else { 9804 // default to VM.log until we have a uuid, then we'll switch. 9805 log = VM.log; 9806 } 9807 9808 log.info('Creating VM, original payload:\n' 9809 + JSON.stringify(payload, null, 2)); 9810 9811 async.waterfall([ 9812 function (cb) { 9813 // We get a UUID first so that we can attach as many log messages 9814 // as possible to this uuid. Since we don't have a UUID here, we 9815 // send VM.log as the logger. We'll switch to a log.child as soon 9816 // as we have uuid. 9817 createZoneUUID(payload, log, function (e, uuid) { 9818 // either payload will have .uuid or we'll return error here. 9819 cb(e); 9820 }); 9821 }, function (cb) { 9822 // If we got here, payload now has .uuid and we can start logging 9823 // messages with that uuid if we didn't already have a logger. 9824 if (!options.hasOwnProperty('log')) { 9825 log = VM.log.child({action: 'create', vm: payload.uuid}); 9826 } 9827 cb(); 9828 }, function (cb) { 9829 normalizePayload(payload, null, log, function (err) { 9830 if (err) { 9831 log.error(err, 'Failed to validate payload: ' 9832 + err.message); 9833 } else { 9834 log.debug('normalized payload:\n' 9835 + JSON.stringify(payload, null, 2)); 9836 } 9837 cb(err); 9838 }); 9839 }, function (cb) { 9840 checkDatasetProvisionable(payload, log, function (provisionable) { 9841 if (!provisionable) { 9842 log.error('checkDatasetProvisionable() says dataset is ' 9843 + 'unprovisionable'); 9844 cb(new Error('provisioning dataset ' + payload.image_uuid 9845 + ' with brand ' + payload.brand 9846 + ' is not supported')); 9847 return; 9848 } 9849 cb(); 9850 }); 9851 }, function (cb) { 9852 if (BRAND_OPTIONS[payload.brand].features.type === 'KVM') { 9853 createVM(payload, log, function (error, result) { 9854 if (error) { 9855 cb(error); 9856 } else { 9857 cb(null, {'uuid': payload.uuid, 9858 'zonename': payload.zonename}); 9859 } 9860 }); 9861 } else { 9862 createZone(payload, log, function (error, result) { 9863 if (error) { 9864 cb(error); 9865 } else { 9866 cb(null, {'uuid': payload.uuid, 9867 'zonename': payload.zonename}); 9868 } 9869 }); 9870 } 9871 } 9872 ], function (err, obj) { 9873 callback(err, obj); 9874 }); 9875 }; 9876 9877 // delete a zvol 9878 function deleteVolume(volume, log, callback) 9879 { 9880 var args; 9881 var origin; 9882 9883 assert(log, 'no logger passed to deleteVolume()'); 9884 9885 if (volume.missing) { 9886 // this volume doesn't actually exist, so skip trying to delete. 9887 log.info('volume ' + volume.path + ' doesn\'t exist, skipping ' 9888 + 'deletion'); 9889 callback(); 9890 return; 9891 } 9892 9893 async.series([ 9894 function (cb) { 9895 args = ['get', '-Ho', 'value', 'origin', volume.zfs_filesystem]; 9896 zfs(args, log, function (err, fds) { 9897 if (err && fds.stderr.match('dataset does not exist')) { 9898 log.info('volume ' + volume.path + ' doesn\'t exist, ' 9899 + 'skipping deletion'); 9900 cb(); 9901 } else { 9902 origin = trim(fds.stdout); 9903 log.info('found origin "' + origin + '"'); 9904 cb(err); 9905 } 9906 }); 9907 }, function (cb) { 9908 // use recursive delete to handle possible snapshots on volume 9909 args = ['destroy', '-rF', volume.zfs_filesystem]; 9910 zfs(args, log, function (err, fds) { 9911 // err will be non-null if something broke 9912 cb(err); 9913 }); 9914 }, function (cb) { 9915 // we never delete an @final snapshot, that's the one from recv 9916 // that imgadm left around for us on purpose. 9917 if (!origin || origin.length < 1 || origin == '-' 9918 || origin.match('@final')) { 9919 9920 cb(); 9921 return; 9922 } 9923 args = ['destroy', '-rF', origin]; 9924 zfs(args, log, function (err, fds) { 9925 // err will be non-null if something broke 9926 cb(err); 9927 }); 9928 } 9929 ], function (err) { 9930 callback(err); 9931 }); 9932 } 9933 9934 function deleteZone(uuid, log, callback) 9935 { 9936 var load_fields; 9937 var vmobj; 9938 9939 assert(log, 'no logger passed to deleteZone()'); 9940 9941 load_fields = [ 9942 'archive_on_delete', 9943 'disks', 9944 'uuid', 9945 'zonename' 9946 ]; 9947 9948 async.series([ 9949 function (cb) { 9950 VM.load(uuid, {fields: load_fields, log: log}, function (err, obj) { 9951 if (err) { 9952 cb(err); 9953 return; 9954 } 9955 vmobj = obj; 9956 cb(); 9957 }); 9958 }, function (cb) { 9959 log.debug('archive_on_delete is set to ' 9960 + !!vmobj.archive_on_delete); 9961 if (!vmobj.archive_on_delete) { 9962 cb(); 9963 return; 9964 } 9965 archiveVM(vmobj.uuid, log, function () { 9966 cb(); 9967 }); 9968 // TODO: replace these next two with VM.stop(..{force: true} ? 9969 }, function (cb) { 9970 log.debug('setting autoboot=false'); 9971 zonecfg(['-u', uuid, 'set autoboot=false'], log, function (e, fds) { 9972 if (e) { 9973 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9974 'Error setting autoboot=false'); 9975 } else { 9976 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 9977 'set autoboot=false'); 9978 } 9979 cb(); 9980 }); 9981 }, function (cb) { 9982 log.debug('halting zone'); 9983 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) { 9984 if (e) { 9985 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9986 'Error halting zone'); 9987 } else { 9988 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 9989 'halted zone'); 9990 } 9991 cb(); 9992 }); 9993 }, function (cb) { 9994 log.debug('uninstalling zone'); 9995 zoneadm(['-u', uuid, 'uninstall', '-F'], log, function (e, fds) { 9996 if (e) { 9997 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 9998 'Error uninstalling zone: ' + e.message); 9999 } else { 10000 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 10001 'uninstalled zone'); 10002 } 10003 cb(); 10004 }); 10005 }, function (cb) { 10006 function loggedDeleteVolume(volume, callbk) { 10007 return deleteVolume(volume, log, callbk); 10008 } 10009 10010 if (vmobj && vmobj.hasOwnProperty('disks')) { 10011 async.forEachSeries(vmobj.disks, loggedDeleteVolume, 10012 function (err) { 10013 if (err) { 10014 log.error(err, 'Unknown error deleting volumes: ' 10015 + err.message); 10016 cb(err); 10017 } else { 10018 log.info('successfully deleted volumes'); 10019 cb(); 10020 } 10021 } 10022 ); 10023 } else { 10024 log.debug('skipping volume destruction for diskless ' 10025 + vmobj.uuid); 10026 cb(); 10027 } 10028 }, function (cb) { 10029 if (vmobj.zonename) { 10030 log.debug('deleting zone'); 10031 // XXX for some reason -u <uuid> doesn't work with delete 10032 zonecfg(['-z', vmobj.zonename, 'delete', '-F'], log, 10033 function (e, fds) { 10034 10035 if (e) { 10036 log.warn({err: e, stdout: fds.stdout, 10037 stderr: fds.stderr}, 'Error deleting VM'); 10038 } else { 10039 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 10040 'deleted VM ' + uuid); 10041 } 10042 cb(); 10043 }); 10044 } else { 10045 cb(); 10046 } 10047 }, function (cb) { 10048 VM.load(uuid, {fields: ['uuid'], log: log, missing_ok: true}, 10049 function (err, obj) { 10050 10051 var gone = /^zoneadm:.*: No such zone configured/; 10052 if (err && err.message.match(gone)) { 10053 // the zone is gone, that's good. 10054 log.debug('confirmed VM is gone.'); 10055 cb(); 10056 } else if (err) { 10057 // there was a non-expected error. 10058 cb(err); 10059 } else { 10060 // the VM still exists! 10061 err = new Error('VM still exists after delete.'); 10062 err.code = 'EEXIST'; 10063 cb(err); 10064 } 10065 }); 10066 }, function (cb) { 10067 // delete the incoming payload if it exists 10068 fs.unlink('/etc/zones/' + vmobj.uuid + '-receiving.json', 10069 function (e) { 10070 // we can't do anyhing if this fails other than log 10071 if (e && e.code !== 'ENOENT') { 10072 log.warn(e, 'Failed to delete ' + vmobj.uuid 10073 + '-receiving.json (' + e.code + '): ' + e.message); 10074 } 10075 cb(); 10076 } 10077 ); 10078 } 10079 ], function (error) { 10080 callback(error); 10081 }); 10082 } 10083 10084 exports.delete = function (uuid, options, callback) 10085 { 10086 var attemptDelete; 10087 var last_try = 16; 10088 var log; 10089 var next_try = 1; 10090 var tries = 0; 10091 10092 // options is optional 10093 if (arguments.length === 2) { 10094 callback = arguments[1]; 10095 options = {}; 10096 } 10097 10098 ensureLogging(true); 10099 10100 if (options.hasOwnProperty('log')) { 10101 log = options.log; 10102 } else { 10103 log = VM.log.child({action: 'delete', vm: uuid}); 10104 } 10105 10106 log.info('Deleting VM ' + uuid); 10107 10108 attemptDelete = function (cb) { 10109 next_try = (next_try * 2); 10110 deleteZone(uuid, log, function (err) { 10111 tries++; 10112 if (err && err.code === 'EEXIST') { 10113 // zone still existed, try again if we've not tried too much. 10114 if (next_try <= last_try) { 10115 log.info('VM.delete(' + tries + '): still there, ' 10116 + 'will try again in: ' + next_try + ' secs'); 10117 setTimeout(function () { 10118 // try again 10119 attemptDelete(cb); 10120 }, next_try * 1000); 10121 } else { 10122 log.warn('VM.delete(' + tries + '): still there after' 10123 + ' ' + next_try + ' seconds, giving up.'); 10124 cb(new Error('delete failed after ' + tries + ' attempts. ' 10125 + '(check the log for details)')); 10126 return; 10127 } 10128 } else if (err) { 10129 // error but not one we can retry from. 10130 log.error(err, 'VM.delete: FATAL: ' + err.message); 10131 cb(err); 10132 } else { 10133 // success! 10134 log.debug('VM.delete: SUCCESS'); 10135 cb(); 10136 } 10137 }); 10138 }; 10139 10140 attemptDelete(function (err) { 10141 if (err) { 10142 log.error(err); 10143 } 10144 callback(err); 10145 }); 10146 }; 10147 10148 // This function needs vmobj to have: 10149 // 10150 // brand 10151 // never_booted 10152 // uuid 10153 // zonename 10154 // 10155 function startZone(vmobj, log, callback) 10156 { 10157 var set_autoboot = 'set autoboot=true'; 10158 var uuid = vmobj.uuid; 10159 10160 assert(log, 'no logger passed to startZone()'); 10161 10162 log.debug('startZone starting ' + uuid); 10163 10164 // 10165 // We set autoboot (or vm-autoboot) here because we've just intentionally 10166 // started this vm, so we want it to come up if the host is rebooted. 10167 // 10168 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 10169 set_autoboot = 'select attr name=vm-autoboot; set value=true; end'; 10170 } 10171 10172 async.series([ 10173 function (cb) { 10174 // do the booting 10175 zoneadm(['-u', uuid, 'boot', '-X'], log, function (err, boot_fds) { 10176 if (err) { 10177 log.error({err: err, stdout: boot_fds.stdout, 10178 stderr: boot_fds.stderr}, 'zoneadm failed to boot ' 10179 + 'VM'); 10180 } else { 10181 log.debug({stdout: boot_fds.stdout, 10182 stderr: boot_fds.stderr}, 'zoneadm booted VM'); 10183 } 10184 cb(err); 10185 }); 10186 }, function (cb) { 10187 // ensure it booted 10188 VM.waitForZoneState(vmobj, 'running', {timeout: 30, log: log}, 10189 function (err, result) { 10190 10191 if (err) { 10192 if (err.code === 'ETIMEOUT') { 10193 log.info(err, 'timeout waiting for zone to go to ' 10194 + '"running"'); 10195 } else { 10196 log.error(err, 'unknown error waiting for zone to go' 10197 + ' "running"'); 10198 } 10199 } else { 10200 // zone got to running 10201 log.info('VM seems to have switched to "running"'); 10202 } 10203 cb(err); 10204 }); 10205 }, function (cb) { 10206 zonecfg(['-u', uuid, set_autoboot], log, 10207 function (err, autoboot_fds) { 10208 10209 if (err) { 10210 // The vm is running at this point, erroring out here would 10211 // do no good, so we just log it. 10212 log.error({err: err, stdout: autoboot_fds.stdout, 10213 stderr: autoboot_fds.stderr}, 'startZone(): Failed to ' 10214 + set_autoboot + ' for ' + uuid); 10215 } else { 10216 log.debug({stdout: autoboot_fds.stdout, 10217 stderr: autoboot_fds.stderr}, 'set autoboot'); 10218 } 10219 cb(err); 10220 }); 10221 }, function (cb) { 10222 if (!vmobj.never_booted) { 10223 cb(); 10224 return; 10225 } 10226 zonecfg(['-u', uuid, 'remove attr name=never-booted' ], log, 10227 function (err, neverbooted_fds) { 10228 // Ignore errors here, because we're started. 10229 if (err) { 10230 log.warn({err: err, stdout: neverbooted_fds.stdout, 10231 stderr: neverbooted_fds.stderr}, 'failed to remove ' 10232 + 'never-booted flag'); 10233 } else { 10234 log.debug({stdout: neverbooted_fds.stdout, 10235 stderr: neverbooted_fds.stderr}, 'removed ' 10236 + 'never-booted flag'); 10237 } 10238 cb(); 10239 } 10240 ); 10241 } 10242 ], function (err) { 10243 if (!err) { 10244 log.info('Started ' + uuid); 10245 } 10246 callback(err); 10247 }); 10248 } 10249 10250 // build the qemu cmdline and start up a VM 10251 // 10252 // vmobj needs any of the following that are defined: 10253 // 10254 // boot 10255 // brand 10256 // cpu_type 10257 // default_gateway 10258 // disks 10259 // hostname 10260 // internal_metadata 10261 // never_booted 10262 // nics 10263 // qemu_extra_opts 10264 // qemu_opts 10265 // ram 10266 // resolvers 10267 // spice_opts 10268 // spice_password 10269 // spice_port 10270 // state 10271 // uuid 10272 // vcpus 10273 // vga 10274 // virtio_txtimer 10275 // virtio_txburst 10276 // vnc_password 10277 // zone_state 10278 // zonename 10279 // zonepath 10280 // 10281 function startVM(vmobj, extra, log, callback) 10282 { 10283 var check_path; 10284 var cmdargs = []; 10285 var d; 10286 var defaultgw = ''; 10287 var disk; 10288 var diskargs = ''; 10289 var disk_idx = 0; 10290 var found; 10291 var hostname = vmobj.uuid; 10292 var mdata; 10293 var nic; 10294 var nic_idx = 0; 10295 var primary_found = false; 10296 var qemu_opts = ''; 10297 var r; 10298 var script; 10299 var spiceargs; 10300 var uuid = vmobj.uuid; 10301 var virtio_txburst; 10302 var virtio_txtimer; 10303 var vnic_opts; 10304 var zoneroot; 10305 10306 assert(log, 'no logger passed to startVM'); 10307 assert(vmobj.hasOwnProperty('zonepath'), 'missing zonepath'); 10308 10309 log.debug('startVM(' + uuid + ')'); 10310 10311 if (!vmobj.hasOwnProperty('state')) { 10312 callback(new Error('Cannot start VM ' + uuid + ' which has no state')); 10313 return; 10314 } 10315 10316 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning') 10317 || (vmobj.state === 'provisioning' 10318 && vmobj.zone_state !== 'installed')) { 10319 10320 callback(new Error('Cannot start VM from state: ' + vmobj.state 10321 + ', must be "stopped"')); 10322 return; 10323 } 10324 10325 zoneroot = path.join(vmobj.zonepath, '/root'); 10326 10327 // We're going to write to /startvm and /tmp/vm.metadata, we don't care if 10328 // they already exist, but we don't want them to be symlinks. 10329 try { 10330 assertSafeZonePath(zoneroot, '/startvm', 10331 {type: 'file', enoent_ok: true}); 10332 assertSafeZonePath(zoneroot, '/tmp/vm.metadata', 10333 {type: 'file', enoent_ok: true}); 10334 } catch (e) { 10335 log.error(e, 'Error validating files for startVM(): ' 10336 + e.message); 10337 callback(e); 10338 return; 10339 } 10340 10341 // XXX TODO: validate vmobj data is ok to start 10342 10343 cmdargs.push('-m', vmobj.ram); 10344 cmdargs.push('-name', vmobj.uuid); 10345 cmdargs.push('-uuid', vmobj.uuid); 10346 10347 if (vmobj.hasOwnProperty('cpu_type')) { 10348 cmdargs.push('-cpu', vmobj.cpu_type); 10349 } else { 10350 cmdargs.push('-cpu', 'qemu64'); 10351 } 10352 10353 if (vmobj.vcpus > 1) { 10354 cmdargs.push('-smp', vmobj.vcpus); 10355 } 10356 10357 for (disk in vmobj.disks) { 10358 if (vmobj.disks.hasOwnProperty(disk)) { 10359 disk = vmobj.disks[disk]; 10360 if (!disk.media) { 10361 disk.media = 'disk'; 10362 } 10363 diskargs = 'file=' + disk.path + ',if=' + disk.model 10364 + ',index=' + disk_idx + ',media=' + disk.media; 10365 if (disk.boot) { 10366 diskargs = diskargs + ',boot=on'; 10367 } 10368 cmdargs.push('-drive', diskargs); 10369 disk_idx++; 10370 } 10371 } 10372 10373 // extra payload can include additional disks that we want to include only 10374 // on this one boot. It can also contain a boot parameter to control boot 10375 // device. See qemu http://qemu.weilnetz.de/qemu-doc.html for info on 10376 // -boot options. 10377 if (extra.hasOwnProperty('disks')) { 10378 for (disk in extra.disks) { 10379 if (extra.disks.hasOwnProperty(disk)) { 10380 disk = extra.disks[disk]; 10381 10382 // ensure this is either a disk that gets mounted in or a 10383 // file that's been dropped in to the zonepath 10384 found = false; 10385 for (d in vmobj.disks) { 10386 if (!found && vmobj.disks.hasOwnProperty(d)) { 10387 d = vmobj.disks[d]; 10388 if (d.path === disk.path) { 10389 found = true; 10390 } 10391 } 10392 } 10393 check_path = path.join(vmobj.zonepath, 'root', disk.path); 10394 if (!found && fs.existsSync(check_path)) { 10395 found = true; 10396 } 10397 if (!found) { 10398 callback(new Error('Cannot find disk: ' + disk.path)); 10399 return; 10400 } 10401 10402 if (!disk.media) { 10403 disk.media = 'disk'; 10404 } 10405 diskargs = 'file=' + disk.path + ',if=' + disk.model 10406 + ',index=' + disk_idx + ',media=' + disk.media; 10407 if (disk.boot) { 10408 diskargs = diskargs + ',boot=on'; 10409 } 10410 cmdargs.push('-drive', diskargs); 10411 disk_idx++; 10412 } 10413 } 10414 } 10415 10416 // helpful values: 10417 // order=nc (network boot, then fallback to disk) 10418 // once=d (boot on disk once and the fallback to default) 10419 // order=c,once=d (boot on CDROM this time, but not subsequent boots) 10420 if (extra.hasOwnProperty('boot')) { 10421 cmdargs.push('-boot', extra.boot); 10422 } else if (vmobj.hasOwnProperty('boot')) { 10423 cmdargs.push('-boot', vmobj.boot); 10424 } else { 10425 // order=cd means try harddisk first (c) and cdrom if that fails (d) 10426 cmdargs.push('-boot', 'order=cd'); 10427 } 10428 10429 if (vmobj.hasOwnProperty('hostname')) { 10430 hostname = vmobj.hostname; 10431 } 10432 10433 if (vmobj.hasOwnProperty('default_gateway')) { 10434 defaultgw = vmobj['default_gateway']; 10435 } 10436 10437 /* 10438 * These tunables are set for all virtio vnics on this VM. 10439 */ 10440 virtio_txtimer = VIRTIO_TXTIMER_DEFAULT; 10441 virtio_txburst = VIRTIO_TXBURST_DEFAULT; 10442 if (vmobj.hasOwnProperty('virtio_txtimer')) { 10443 virtio_txtimer = vmobj.virtio_txtimer; 10444 } 10445 if (vmobj.hasOwnProperty('virtio_txburst')) { 10446 virtio_txburst = vmobj.virtio_txburst; 10447 } 10448 10449 for (nic in vmobj.nics) { 10450 if (vmobj.nics.hasOwnProperty(nic)) { 10451 nic = vmobj.nics[nic]; 10452 10453 // for virtio devices, we want to be able to set the txtimer and 10454 // txburst so we use a '-device' instead of a '-net' line. 10455 if (nic.model === 'virtio') { 10456 cmdargs.push('-device', 10457 'virtio-net-pci,mac=' + nic.mac 10458 + ',tx=timer,x-txtimer=' + virtio_txtimer 10459 + ',x-txburst=' + virtio_txburst 10460 + ',vlan=' + nic_idx); 10461 } else { 10462 cmdargs.push('-net', 10463 'nic,macaddr=' + nic.mac 10464 + ',vlan=' + nic_idx 10465 + ',name=' + nic.interface 10466 + ',model=' + nic.model); 10467 } 10468 vnic_opts = 'vnic,name=' + nic.interface 10469 + ',vlan=' + nic_idx 10470 + ',ifname=' + nic.interface; 10471 10472 if (nic.ip != 'dhcp') { 10473 vnic_opts = vnic_opts 10474 + ',ip=' + nic.ip 10475 + ',netmask=' + nic.netmask; 10476 } 10477 10478 // The primary network provides the resolvers, default gateway 10479 // and hostname to prevent vm from trying to use settings 10480 // from more than one nic 10481 if (!primary_found) { 10482 if (nic.hasOwnProperty('primary') && nic.primary) { 10483 if (nic.hasOwnProperty('gateway') && nic.ip != 'dhcp') { 10484 vnic_opts += ',gateway_ip=' + nic.gateway; 10485 } 10486 primary_found = true; 10487 } else if (defaultgw && nic.hasOwnProperty('gateway') 10488 && nic.gateway == defaultgw) { 10489 10490 /* 10491 * XXX this exists here for backward compatibilty. New VMs 10492 * and old VMs that are upgraded should not use 10493 * default_gateway. When we've implemented autoupgrade 10494 * this block (and all reference to default_gateway) 10495 * can be removed. 10496 */ 10497 10498 if (nic.ip != 'dhcp') { 10499 vnic_opts += ',gateway_ip=' + nic.gateway; 10500 } 10501 primary_found = true; 10502 } 10503 10504 if (primary_found && nic.ip != 'dhcp') { 10505 if (hostname) { 10506 vnic_opts += ',hostname=' + hostname; 10507 } 10508 if (vmobj.hasOwnProperty('resolvers')) { 10509 for (r in vmobj.resolvers) { 10510 vnic_opts += ',dns_ip' + r + '=' 10511 + vmobj.resolvers[r]; 10512 } 10513 } 10514 } 10515 } 10516 10517 cmdargs.push('-net', vnic_opts); 10518 nic_idx++; 10519 } 10520 } 10521 10522 cmdargs.push('-smbios', 'type=1,manufacturer=Joyent,' 10523 + 'product=SmartDC HVM,version=6.2012Q1,' 10524 + 'serial=' + vmobj.uuid + ',uuid=' + vmobj.uuid + ',' 10525 + 'sku=001,family=Virtual Machine'); 10526 10527 cmdargs.push('-pidfile', '/tmp/vm.pid'); 10528 10529 if (vmobj.hasOwnProperty('vga')) { 10530 cmdargs.push('-vga', vmobj.vga); 10531 } else { 10532 cmdargs.push('-vga', 'std'); 10533 } 10534 10535 cmdargs.push('-chardev', 10536 'socket,id=qmp,path=/tmp/vm.qmp,server,nowait'); 10537 cmdargs.push('-qmp', 'chardev:qmp'); 10538 10539 // serial0 is for serial console 10540 cmdargs.push('-chardev', 10541 'socket,id=serial0,path=/tmp/vm.console,server,nowait'); 10542 cmdargs.push('-serial', 'chardev:serial0'); 10543 10544 // serial1 is used for metadata API 10545 cmdargs.push('-chardev', 10546 'socket,id=serial1,path=/tmp/vm.ttyb,server,nowait'); 10547 cmdargs.push('-serial', 'chardev:serial1'); 10548 10549 if (!vmobj.qemu_opts) { 10550 if (vmobj.hasOwnProperty('vnc_password') 10551 && vmobj.vnc_password.length > 0) { 10552 10553 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc,password'); 10554 } else { 10555 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc'); 10556 } 10557 if (vmobj.hasOwnProperty('spice_port') 10558 && vmobj.spice_port !== -1) { 10559 10560 spiceargs = 'sock=/tmp/vm.spice'; 10561 if (!vmobj.hasOwnProperty('spice_password') 10562 || vmobj.spice_password.length <= 0) { 10563 10564 spiceargs = spiceargs + ',disable-ticketing'; 10565 10566 // Otherwise, spice password is set via qmp, so we don't 10567 // need to do anything here 10568 } 10569 if (vmobj.hasOwnProperty('spice_opts') 10570 && vmobj.spice_opts.length > 0) { 10571 10572 spiceargs = spiceargs + ',' + vmobj.spice_opts; 10573 } 10574 cmdargs.push('-spice', spiceargs); 10575 } 10576 cmdargs.push('-parallel', 'none'); 10577 cmdargs.push('-usb'); 10578 cmdargs.push('-usbdevice', 'tablet'); 10579 cmdargs.push('-k', 'en-us'); 10580 } else { 10581 qemu_opts = vmobj.qemu_opts.toString(); 10582 } 10583 10584 if (vmobj.qemu_extra_opts) { 10585 qemu_opts = qemu_opts + ' ' + vmobj.qemu_extra_opts; 10586 } 10587 10588 // This actually creates the qemu process 10589 script = '#!/usr/bin/bash\n\n' 10590 + 'exec >/tmp/vm.startvm.log 2>&1\n\n' 10591 + 'set -o xtrace\n\n' 10592 + 'if [[ -x /startvm.zone ]]; then\n' 10593 + ' exec /smartdc/bin/qemu-exec /startvm.zone "' 10594 + cmdargs.join('" "') 10595 + '" ' + qemu_opts + '\n' 10596 + 'else\n' 10597 + ' exec /smartdc/bin/qemu-exec /smartdc/bin/qemu-system-x86_64 "' 10598 + cmdargs.join('" "') 10599 + '" ' + qemu_opts + '\n' 10600 + 'fi\n\n' 10601 + 'exit 1\n'; 10602 10603 try { 10604 fs.writeFileSync(vmobj.zonepath + '/root/startvm', script); 10605 fs.chmodSync(vmobj.zonepath + '/root/startvm', '0755'); 10606 } catch (e) { 10607 log.warn(e, 'Unable to create /startvm script in ' + vmobj.uuid); 10608 callback(new Error('cannot create /startvm')); 10609 return; 10610 } 10611 10612 mdata = { 10613 'internal_metadata': 10614 vmobj.internal_metadata ? vmobj.internal_metadata : {} 10615 }; 10616 fs.writeFile(path.join(vmobj.zonepath, '/root/tmp/vm.metadata'), 10617 JSON.stringify(mdata, null, 2) + '\n', 10618 function (err) { 10619 if (err) { 10620 log.debug(err, 'FAILED TO write metadata to ' 10621 + '/tmp/vm.metadata: ' + err.message); 10622 callback(err); 10623 } else { 10624 log.debug('wrote metadata to /tmp/vm.metadata'); 10625 startZone(vmobj, log, callback); 10626 } 10627 } 10628 ); 10629 } 10630 10631 // according to usr/src/common/zfs/zfs_namecheck.c allowed characters are: 10632 // 10633 // alphanumeric characters plus the following: [-_.:%] 10634 // 10635 function validSnapshotName(snapname, log) 10636 { 10637 assert(log, 'no logger passed to validSnapshotName()'); 10638 10639 if (snapname.length < 1 || snapname.length > MAX_SNAPNAME_LENGTH) { 10640 log.error('Invalid snapname length: ' + snapname.length 10641 + ' valid range: [1-' + MAX_SNAPNAME_LENGTH + ']'); 10642 return (false); 10643 } 10644 10645 if (snapname.match(/[^a-zA-Z0-9\-\_\.\:\%]/)) { 10646 log.error('Invalid snapshot name: contains invalid characters.'); 10647 return (false); 10648 } 10649 10650 return (true); 10651 } 10652 10653 function performSnapshotRollback(snapshots, log, callback) 10654 { 10655 assert(log, 'no logger passed to performSnapshotRollback()'); 10656 10657 // NOTE: we assume machine is stopped and snapshots are already validated 10658 10659 function rollback(snapname, cb) { 10660 var args; 10661 10662 args = ['rollback', '-r', snapname]; 10663 zfs(args, log, function (zfs_err, fds) { 10664 if (zfs_err) { 10665 log.error({'err': zfs_err, 'stdout': fds.stdout, 10666 'stderr': fds.stdout}, 'zfs rollback of ' + snapname 10667 + ' failed.'); 10668 cb(zfs_err); 10669 return; 10670 } 10671 log.info('rolled back snapshot ' + snapname); 10672 log.debug('zfs destroy stdout: ' + fds.stdout); 10673 log.debug('zfs destroy stderr: ' + fds.stderr); 10674 cb(); 10675 }); 10676 } 10677 10678 async.forEachSeries(snapshots, rollback, function (err) { 10679 if (err) { 10680 log.error(err, 'Unable to rollback some datasets.'); 10681 } 10682 callback(err); 10683 }); 10684 } 10685 10686 function updateZonecfgTimestamp(vmobj, callback) 10687 { 10688 var file; 10689 var now; 10690 10691 assert(vmobj.zonename, 'updateZonecfgTimestamp() vmobj must have ' 10692 + '.zonename'); 10693 10694 file = path.join('/etc/zones/', vmobj.zonename + '.xml'); 10695 now = new Date(); 10696 10697 fs.utimes(file, now, now, callback); 10698 } 10699 10700 exports.rollback_snapshot = function (uuid, snapname, options, callback) 10701 { 10702 var load_fields; 10703 var log; 10704 10705 // options is optional 10706 if (arguments.length === 3) { 10707 callback = arguments[2]; 10708 options = {}; 10709 } 10710 10711 ensureLogging(true); 10712 if (options.hasOwnProperty('log')) { 10713 log = options.log; 10714 } else { 10715 log = VM.log.child({action: 'rollback_snapshot', vm: uuid}); 10716 } 10717 10718 if (!validSnapshotName(snapname, log)) { 10719 callback(new Error('Invalid snapshot name')); 10720 return; 10721 } 10722 10723 load_fields = [ 10724 'brand', 10725 'snapshots', 10726 'zfs_filesystem', 10727 'state', 10728 'uuid' 10729 ]; 10730 10731 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10732 var found; 10733 var snap; 10734 var snapshot_list = []; 10735 10736 if (err) { 10737 callback(err); 10738 return; 10739 } 10740 10741 if (vmobj.brand === 'kvm') { 10742 callback(new Error('snapshots for KVM VMs currently unsupported')); 10743 return; 10744 } 10745 10746 found = false; 10747 if (vmobj.hasOwnProperty('snapshots')) { 10748 for (snap in vmobj.snapshots) { 10749 if (vmobj.snapshots[snap].name === snapname) { 10750 found = true; 10751 break; 10752 } 10753 } 10754 } 10755 if (!found) { 10756 callback(new Error('No snapshot named "' + snapname + '" for ' 10757 + uuid)); 10758 return; 10759 } 10760 10761 snapshot_list = [vmobj.zfs_filesystem + '@vmsnap-' + snapname]; 10762 10763 if (vmobj.state !== 'stopped') { 10764 VM.stop(vmobj.uuid, {'force': true, log: log}, function (stop_err) { 10765 if (stop_err) { 10766 log.error(stop_err, 'failed to stop VM ' + vmobj.uuid 10767 + ': ' + stop_err.message); 10768 callback(stop_err); 10769 return; 10770 } 10771 performSnapshotRollback(snapshot_list, log, 10772 function (rollback_err) { 10773 10774 if (rollback_err) { 10775 log.error(rollback_err, 'failed to ' 10776 + 'performSnapshotRollback'); 10777 callback(rollback_err); 10778 return; 10779 } 10780 if (options.do_not_start) { 10781 callback(); 10782 } else { 10783 VM.start(vmobj.uuid, {}, {log: log}, callback); 10784 } 10785 return; 10786 }); 10787 }); 10788 } else { 10789 performSnapshotRollback(snapshot_list, log, 10790 function (rollback_err) { 10791 10792 if (rollback_err) { 10793 log.error(rollback_err, 'failed to ' 10794 + 'performSnapshotRollback'); 10795 callback(rollback_err); 10796 return; 10797 } 10798 if (options.do_not_start) { 10799 callback(); 10800 } else { 10801 VM.start(vmobj.uuid, {}, {log: log}, callback); 10802 } 10803 return; 10804 }); 10805 } 10806 }); 10807 }; 10808 10809 exports.delete_snapshot = function (uuid, snapname, options, callback) 10810 { 10811 var load_fields; 10812 var log; 10813 10814 // options is optional 10815 if (arguments.length === 3) { 10816 callback = arguments[2]; 10817 options = {}; 10818 } 10819 10820 ensureLogging(true); 10821 if (options.hasOwnProperty('log')) { 10822 log = options.log; 10823 } else { 10824 log = VM.log.child({action: 'delete_snapshot', vm: uuid}); 10825 } 10826 10827 if (!validSnapshotName(snapname, log)) { 10828 callback(new Error('Invalid snapshot name')); 10829 return; 10830 } 10831 10832 load_fields = [ 10833 'brand', 10834 'snapshots', 10835 'zfs_filesystem', 10836 'zonepath', 10837 'zonename' 10838 ]; 10839 10840 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10841 var found; 10842 var mountpath; 10843 var mountpoint; 10844 var snap; 10845 var zoneroot; 10846 10847 if (err) { 10848 callback(err); 10849 return; 10850 } 10851 10852 if (vmobj.brand === 'kvm') { 10853 callback(new Error('snapshots for KVM VMs currently unsupported')); 10854 return; 10855 } 10856 10857 found = false; 10858 if (vmobj.hasOwnProperty('snapshots')) { 10859 for (snap in vmobj.snapshots) { 10860 if (vmobj.snapshots[snap].name === snapname) { 10861 found = true; 10862 break; 10863 } 10864 } 10865 } 10866 if (!found) { 10867 callback(new Error('No snapshot named "' + snapname + '" for ' 10868 + uuid)); 10869 return; 10870 } 10871 10872 zoneroot = vmobj.zonepath + '/root'; 10873 mountpath = '/checkpoints/' + snapname; 10874 mountpoint = zoneroot + '/' + mountpath; 10875 10876 async.waterfall([ 10877 function (cb) { 10878 // Ensure it's safe for us to be doing something in this dir 10879 try { 10880 assertSafeZonePath(zoneroot, mountpath, 10881 {type: 'dir', enoent_ok: true}); 10882 } catch (e) { 10883 log.error(e, 'Unsafe mountpoint for checkpoints: ' 10884 + e.message); 10885 cb(e); 10886 return; 10887 } 10888 cb(); 10889 }, function (cb) { 10890 // umount snapshot 10891 var argv; 10892 var cmd = '/usr/sbin/umount'; 10893 10894 argv = [mountpoint]; 10895 10896 execFile(cmd, argv, function (e, stdout, stderr) { 10897 if (e) { 10898 log.error({err: e}, 'There was an error while ' 10899 + 'unmounting the snapshot: ' + e.message); 10900 // we treat an error here as fatal only if the error 10901 // was something other than 'not mounted' 10902 if (!stderr.match(/ not mounted/)) { 10903 cb(e); 10904 return; 10905 } 10906 } else { 10907 log.trace('umounted ' + mountpoint); 10908 } 10909 cb(); 10910 }); 10911 }, function (cb) { 10912 // remove the mountpoint directory 10913 fs.rmdir(mountpoint, function (e) { 10914 if (e) { 10915 log.error(e); 10916 } else { 10917 log.trace('removed directory ' + mountpoint); 10918 } 10919 cb(); // XXX not fatal because might also not exist 10920 }); 10921 }, function (cb) { 10922 var args; 10923 10924 args = ['destroy', vmobj.zfs_filesystem + '@vmsnap-' 10925 + snapname]; 10926 10927 zfs(args, log, function (e, fds) { 10928 if (e) { 10929 log.error({'err': e, 'stdout': fds.stdout, 10930 'stderr': fds.stdout}, 'zfs destroy failed.'); 10931 cb(e); 10932 return; 10933 } 10934 log.debug({err: e, stdout: fds.stdout, stderr: fds.stderr}, 10935 'zfs destroy ' + vmobj.zfs_filesystem + '@vmsnap-' 10936 + snapname); 10937 cb(); 10938 }); 10939 }, function (cb) { 10940 updateZonecfgTimestamp(vmobj, function (e) { 10941 if (e) { 10942 log.warn(e, 'failed to update timestamp after deleting ' 10943 + 'snapshot'); 10944 } 10945 // don't pass err because there's no recovery possible 10946 // (the snapshot's gone) 10947 cb(); 10948 }); 10949 } 10950 ], function (error) { 10951 callback(error); 10952 }); 10953 }); 10954 }; 10955 10956 exports.create_snapshot = function (uuid, snapname, options, callback) 10957 { 10958 var load_fields; 10959 var log; 10960 10961 // options is optional 10962 if (arguments.length === 3) { 10963 callback = arguments[2]; 10964 options = {}; 10965 } 10966 10967 ensureLogging(true); 10968 10969 if (options.hasOwnProperty('log')) { 10970 log = options.log; 10971 } else { 10972 log = VM.log.child({action: 'create_snapshot', vm: uuid}); 10973 } 10974 10975 if (!validSnapshotName(snapname, log)) { 10976 callback(new Error('Invalid snapshot name')); 10977 return; 10978 } 10979 10980 load_fields = [ 10981 'brand', 10982 'datasets', 10983 'zone_state', 10984 'snapshots', 10985 'zfs_filesystem', 10986 'zonepath', 10987 'zonename' 10988 ]; 10989 10990 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 10991 var full_snapname; 10992 var mountpath; 10993 var mountpoint; 10994 var mount_snapshot = true; 10995 var snap; 10996 var snapshot_list = []; 10997 var zoneroot; 10998 10999 if (err) { 11000 callback(err); 11001 return; 11002 } 11003 11004 if (vmobj.brand === 'kvm') { 11005 callback(new Error('snapshots for KVM VMs currently unsupported')); 11006 return; 11007 } 11008 11009 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 0) { 11010 callback(new Error('Cannot currently snapshot zones that have ' 11011 + 'datasets')); 11012 return; 11013 } 11014 11015 if (!vmobj.hasOwnProperty('zfs_filesystem')) { 11016 callback(new Error('vmobj missing zfs_filesystem, cannot create ' 11017 + 'snapshot')); 11018 return; 11019 } 11020 11021 full_snapname = vmobj.zfs_filesystem + '@vmsnap-' + snapname; 11022 11023 // Check that name not already used 11024 if (vmobj.hasOwnProperty('snapshots')) { 11025 for (snap in vmobj.snapshots) { 11026 snap = vmobj.snapshots[snap]; 11027 11028 if (snap.name === full_snapname) { 11029 callback(new Error('snapshot with name "' + snapname 11030 + '" already exists.')); 11031 return; 11032 } else { 11033 log.debug('SKIPPING ' + snap.name); 11034 } 11035 } 11036 } 11037 11038 snapshot_list.push(full_snapname); 11039 11040 // assert snapshot_list.length > 0 11041 11042 log.info('Taking snapshot "' + snapname + '" of ' + uuid); 11043 11044 zoneroot = vmobj.zonepath + '/root'; 11045 mountpath = '/checkpoints/' + snapname; 11046 mountpoint = zoneroot + '/' + mountpath; 11047 11048 async.waterfall([ 11049 function (cb) { 11050 // take the snapshot 11051 var args; 11052 args = ['snapshot'].concat(snapshot_list); 11053 11054 zfs(args, log, function (zfs_err, fds) { 11055 if (zfs_err) { 11056 log.error({err: zfs_err, stdout: fds.stdout, 11057 stderr: fds.stdout}, 'zfs snapshot failed.'); 11058 } else { 11059 log.debug({err: zfs_err, stdout: fds.stdout, 11060 stderr: fds.stderr}, 'zfs ' + args.join(' ')); 11061 } 11062 cb(zfs_err); 11063 }); 11064 }, function (cb) { 11065 11066 if (vmobj.zone_state !== 'running') { 11067 log.info('Not mounting snapshot as zone is in state ' 11068 + vmobj.zone_state + ', must be: running'); 11069 mount_snapshot = false; 11070 cb(); 11071 return; 11072 } 11073 11074 // Ensure it's safe for us to be doing something in this dir 11075 try { 11076 assertSafeZonePath(zoneroot, mountpath, 11077 {type: 'dir', enoent_ok: true}); 11078 } catch (e) { 11079 log.error(e, 'Unsafe mountpoint for checkpoints: ' 11080 + e.message); 11081 cb(e); 11082 return; 11083 } 11084 cb(); 11085 }, function (cb) { 11086 // Make the mountpoint directory and parent 11087 var newmode; 11088 11089 if (mount_snapshot === false) { 11090 cb(); 11091 return; 11092 } 11093 11094 /*jsl:ignore*/ 11095 newmode = 0755; 11096 /*jsl:end*/ 11097 11098 function doMkdir(dir, callbk) { 11099 fs.mkdir(dir, newmode, function (e) { 11100 if (e && e.code !== 'EEXIST') { 11101 log.error({err: e}, 'unable to create mountpoint ' 11102 + 'for checkpoints: ' + e.message); 11103 callbk(e); 11104 return; 11105 } 11106 callbk(); 11107 }); 11108 } 11109 11110 doMkdir(path.dirname(mountpoint), function (parent_e) { 11111 if (parent_e) { 11112 cb(parent_e); 11113 return; 11114 } 11115 doMkdir(mountpoint, function (dir_e) { 11116 if (dir_e) { 11117 cb(dir_e); 11118 return; 11119 } 11120 11121 log.debug('created ' + mountpoint); 11122 cb(); 11123 }); 11124 }); 11125 }, function (cb) { 11126 var argv; 11127 var cmd = '/usr/sbin/mount'; 11128 var snapdir; 11129 11130 if (mount_snapshot === false) { 11131 cb(); 11132 return; 11133 } 11134 11135 snapdir = vmobj.zonepath + '/.zfs/snapshot/vmsnap-' + snapname 11136 + '/root'; 11137 argv = [ '-F', 'lofs', '-o', 'ro,setuid,nodevices', snapdir, 11138 mountpoint]; 11139 11140 execFile(cmd, argv, function (e, stdout, stderr) { 11141 if (e) { 11142 log.error({err: e}, 'unable to mount snapshot: ' 11143 + e.message); 11144 } 11145 // not fatal becase snapshot was already created. 11146 cb(); 11147 }); 11148 }, function (cb) { 11149 // update timestamp so last_modified gets bumped 11150 updateZonecfgTimestamp(vmobj, function (e) { 11151 if (e) { 11152 log.warn(e, 11153 'failed to update timestamp after snapshot'); 11154 } 11155 // ignore error since there's no recovery 11156 // (snapshot was created) 11157 cb(); 11158 }); 11159 } 11160 ], function (error) { 11161 callback(error); 11162 }); 11163 }); 11164 }; 11165 11166 exports.start = function (uuid, extra, options, callback) 11167 { 11168 var load_fields; 11169 var log; 11170 11171 load_fields = [ 11172 'brand', 11173 'nics', 11174 'state', 11175 'uuid', 11176 'zone_state', 11177 'zonename', 11178 'zonepath' 11179 ]; 11180 11181 // options is optional 11182 if (arguments.length === 3) { 11183 callback = arguments[2]; 11184 options = {}; 11185 } 11186 11187 assert(callback, 'undefined callback!'); 11188 11189 ensureLogging(true); 11190 if (options.hasOwnProperty('log')) { 11191 log = options.log; 11192 } else { 11193 log = VM.log.child({action: 'start', vm: uuid}); 11194 } 11195 11196 log.info('Starting VM ' + uuid); 11197 11198 VM.load(uuid, {log: log, fields: load_fields}, function (err, vmobj) { 11199 if (err) { 11200 callback(err); 11201 } else { 11202 11203 if (vmobj.state === 'running') { 11204 err = new Error('VM ' + vmobj.uuid + ' is already ' 11205 + '\'running\''); 11206 err.code = 'EALREADYRUNNING'; 11207 callback(err); 11208 return; 11209 } 11210 11211 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning') 11212 || (vmobj.state === 'provisioning' 11213 && vmobj.zone_state !== 'installed')) { 11214 11215 err = new Error('Cannot to start vm from state "' + vmobj.state 11216 + '", must be "stopped".'); 11217 log.error(err); 11218 callback(err); 11219 return; 11220 } 11221 11222 lookupInvalidNicTags(vmobj.nics, log, function (e) { 11223 var kvm_load_fields = [ 11224 'boot', 11225 'brand', 11226 'cpu_type', 11227 'default_gateway', 11228 'disks', 11229 'hostname', 11230 'internal_metadata', 11231 'never_booted', 11232 'nics', 11233 'qemu_extra_opts', 11234 'qemu_opts', 11235 'ram', 11236 'resolvers', 11237 'spice_opts', 11238 'spice_password', 11239 'spice_port', 11240 'state', 11241 'uuid', 11242 'vcpus', 11243 'vga', 11244 'virtio_txtimer', 11245 'virtio_txburst', 11246 'vnc_password', 11247 'zone_state', 11248 'zonename', 11249 'zonepath' 11250 ]; 11251 11252 if (e) { 11253 callback(e); 11254 return; 11255 } 11256 11257 if (BRAND_OPTIONS[vmobj.brand].features.type === 'KVM') { 11258 // when we boot KVM we need a lot more fields, so load again 11259 // in that case to get the fields we need. 11260 VM.load(uuid, {log: log, fields: kvm_load_fields}, 11261 function (error, obj) { 11262 11263 if (error) { 11264 callback(error); 11265 return; 11266 } 11267 startVM(obj, extra, log, callback); 11268 }); 11269 } else if (BRAND_OPTIONS[vmobj.brand].features.type === 'OS') { 11270 startZone(vmobj, log, callback); 11271 } else { 11272 err = new Error('no idea how to start a vm with brand: ' 11273 + vmobj.brand); 11274 log.error(err); 11275 callback(err); 11276 } 11277 }); 11278 } 11279 }); 11280 }; 11281 11282 function setRctl(zonename, rctl, value, log, callback) 11283 { 11284 var args; 11285 11286 assert(log, 'no logger passed to setRctl()'); 11287 11288 args = ['-n', rctl, '-v', value.toString(), '-r', '-i', 'zone', zonename]; 11289 log.debug('/usr/bin/prctl ' + args.join(' ')); 11290 execFile('/usr/bin/prctl', args, function (error, stdout, stderr) { 11291 if (error) { 11292 log.error(error, 'setRctl() failed with: ' + stderr); 11293 callback(error); 11294 } else { 11295 callback(); 11296 } 11297 }); 11298 } 11299 11300 function resizeTmp(zonename, newsize, log, callback) 11301 { 11302 var args; 11303 11304 // NOTE: this used to update /etc/vfstab in the zone as well, but was 11305 // changed with OS-920. Now vfstab is updated by mdata-fetch in the 11306 // zone instead, so that will happen next boot. We still do the mount 11307 // so the property update happens on the running zone. 11308 11309 assert(log, 'no logger passed to resizeTmp()'); 11310 11311 args = [zonename, '/usr/sbin/mount', '-F', 'tmpfs', '-o', 'remount,size=' 11312 + newsize + 'm', '/tmp']; 11313 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 11314 execFile('/usr/sbin/zlogin', args, function (err, mnt_stdout, mnt_stderr) { 11315 if (err) { 11316 log.error({'err': err, 'stdout': mnt_stdout, 11317 'stderr': mnt_stderr}, 'zlogin for ' + zonename 11318 + ' exited with code ' + err.code + ' -- ' + err.message); 11319 // error here is not fatal as this should be fixed on reboot 11320 } 11321 11322 callback(); 11323 }); 11324 } 11325 11326 function resizeDisks(disks, updates, log, callback) 11327 { 11328 var d; 11329 var disk; 11330 var resized = 0; 11331 var vols = []; 11332 11333 assert(log, 'no logger passed to resizeDisks()'); 11334 11335 for (disk in updates) { 11336 disk = updates[disk]; 11337 for (d in disks) { 11338 d = disks[d]; 11339 if (d.path === disk.path && disk.hasOwnProperty('size')) { 11340 vols.push({'disk': d, 'new_size': disk.size}); 11341 } 11342 } 11343 } 11344 11345 function resize(vol, cb) { 11346 var args; 11347 var dsk = vol.disk; 11348 var size = vol.new_size; 11349 11350 if (dsk.hasOwnProperty('zfs_filesystem')) { 11351 if (dsk.size > size) { 11352 cb(new Error('cannot resize ' + dsk.zfs_filesystem 11353 + ' new size must be greater than current size. (' 11354 + dsk.size + ' > ' + dsk.size + ')')); 11355 } else if (dsk.size === size) { 11356 // no point resizing if the old+new are the same 11357 cb(); 11358 } else { 11359 args = ['set', 'volsize=' + size + 'M', dsk.zfs_filesystem]; 11360 zfs(args, log, function (err, fds) { 11361 resized++; 11362 cb(err); 11363 }); 11364 } 11365 } else { 11366 cb(new Error('could not find zfs_filesystem in ' 11367 + JSON.stringify(dsk))); 11368 } 11369 } 11370 11371 async.forEachSeries(vols, resize, function (err) { 11372 if (err) { 11373 log.error(err, 'Unable to resize disks'); 11374 callback(err); 11375 } else { 11376 callback(null, resized); 11377 } 11378 }); 11379 } 11380 11381 function updateVnicAllowedIPs(uuid, nic, log, callback) 11382 { 11383 var ips = []; 11384 11385 assert(log, 'no logger passed to updateVnicAllowedIPs()'); 11386 11387 if (!uuid || !nic.interface) { 11388 callback(); 11389 return; 11390 } 11391 11392 if (nic.hasOwnProperty('allow_ip_spoofing') && nic.allow_ip_spoofing) { 11393 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback); 11394 return; 11395 } 11396 11397 if (nic.hasOwnProperty('ip')) { 11398 ips.push(nic.ip); 11399 } 11400 11401 if (nic.hasOwnProperty('vrrp_primary_ip')) { 11402 ips.push(nic.vrrp_primary_ip); 11403 } 11404 11405 if (nic.hasOwnProperty('allowed_ips')) { 11406 ips = ips.concat(nic.allowed_ips); 11407 } 11408 11409 if (!ips.length === 0) { 11410 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback); 11411 } else { 11412 dladm.setLinkProp(uuid, nic.interface, 'allowed-ips', ips, log, 11413 callback); 11414 } 11415 } 11416 11417 function updateVnicProperties(uuid, vmobj, payload, log, callback) 11418 { 11419 assert(log, 'no logger passed to updateVnicProperties()'); 11420 11421 if (vmobj.state != 'running') { 11422 log.debug('VM not running: not updating vnic properties'); 11423 callback(null); 11424 return; 11425 } 11426 11427 if (!payload.hasOwnProperty('update_nics')) { 11428 log.debug( 11429 'No update_nics property: not updating vnic properties'); 11430 callback(null); 11431 return; 11432 } 11433 11434 async.forEach(payload.update_nics, function (nic, cb) { 11435 var opt; 11436 var needsUpdate = false; 11437 var needsIPupdate = false; 11438 var spoof_opts = { 11439 'allow_ip_spoofing': 'ip-nospoof', 11440 'allow_mac_spoofing': 'mac-nospoof', 11441 'allow_dhcp_spoofing': 'dhcp-nospoof', 11442 'allow_restricted_traffic': 'restricted' 11443 }; 11444 var vm_nic; 11445 11446 // First, determine if we've changed any of the spoofing opts in this 11447 // update: 11448 for (opt in spoof_opts) { 11449 if (nic.hasOwnProperty(opt)) { 11450 needsUpdate = true; 11451 break; 11452 } 11453 } 11454 11455 if (nic.hasOwnProperty('vrrp_primary_ip') 11456 || nic.hasOwnProperty('allowed_ips') 11457 || nic.hasOwnProperty('allow_ip_spoofing')) { 11458 needsIPupdate = true; 11459 } 11460 11461 for (vm_nic in vmobj.nics) { 11462 vm_nic = vmobj.nics[vm_nic]; 11463 if (vm_nic.mac == nic.mac) { 11464 break; 11465 } 11466 } 11467 11468 if (!vm_nic) { 11469 cb(new Error('Unknown NIC: ' + nic.mac)); 11470 return; 11471 } 11472 11473 if (!needsUpdate) { 11474 log.debug('No spoofing / allowed IP opts updated for nic "' 11475 + nic.mac + '": not updating'); 11476 if (needsIPupdate) { 11477 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11478 } else { 11479 cb(null); 11480 } 11481 return; 11482 } 11483 11484 // Using the updated nic object, figure out what spoofing opts to set 11485 for (opt in spoof_opts) { 11486 if (vm_nic.hasOwnProperty(opt) && fixBoolean(vm_nic[opt])) { 11487 delete spoof_opts[opt]; 11488 } 11489 } 11490 11491 if (vm_nic.hasOwnProperty('dhcp_server') 11492 && fixBoolean(vm_nic.dhcp_server)) { 11493 delete spoof_opts.allow_dhcp_spoofing; 11494 delete spoof_opts.allow_ip_spoofing; 11495 } 11496 11497 if (Object.keys(spoof_opts).length === 0) { 11498 dladm.resetLinkProp(uuid, vm_nic.interface, 'protection', log, 11499 function (err) { 11500 if (err) { 11501 cb(err); 11502 return; 11503 } 11504 if (needsIPupdate) { 11505 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11506 return; 11507 } 11508 cb(); 11509 return; 11510 }); 11511 } else { 11512 dladm.setLinkProp(uuid, vm_nic.interface, 'protection', 11513 Object.keys(spoof_opts).map(function (k) { 11514 return spoof_opts[k]; 11515 }), log, 11516 function (err) { 11517 if (err) { 11518 cb(err); 11519 return; 11520 } 11521 if (needsIPupdate) { 11522 updateVnicAllowedIPs(uuid, vm_nic, log, cb); 11523 return; 11524 } 11525 cb(); 11526 return; 11527 }); 11528 } 11529 }, function (err) { 11530 if (err) { 11531 callback(err); 11532 } else { 11533 callback(null); 11534 } 11535 }); 11536 } 11537 11538 // Run a fw.js function that requires all VM records 11539 function firewallVMrun(opts, fn, log, callback) 11540 { 11541 assert(log, 'no logger passed to firewallVMrun()'); 11542 VM.lookup({}, {fields: fw.VM_FIELDS, log: log}, function (err, records) { 11543 if (err) { 11544 callback(err); 11545 return; 11546 } 11547 11548 opts.vms = records; 11549 if (fn.name == 'validatePayload') { 11550 opts.logName = 'VM-create'; 11551 } else { 11552 opts.logName = 'VM-' + (fn.name || ''); 11553 } 11554 11555 if (opts.provisioning) { 11556 opts.vms.push(opts.provisioning); 11557 delete opts.provisioning; 11558 } 11559 11560 fn(opts, callback); 11561 return; 11562 }); 11563 } 11564 11565 function validateFirewall(payload, log, callback) 11566 { 11567 assert(log, 'no logger passed to validateFirewall()'); 11568 11569 log.debug(toValidate, 'Validating firewall payload'); 11570 var toValidate = payload.firewall; 11571 toValidate.provisioning = { 11572 'state': 'provisioning' 11573 }; 11574 11575 fw.VM_FIELDS.forEach(function (field) { 11576 if (payload.hasOwnProperty(field)) { 11577 toValidate.provisioning[field] = payload[field]; 11578 } 11579 }); 11580 11581 if (payload.hasOwnProperty('add_nics')) { 11582 toValidate.provisioning.nics = payload.add_nics; 11583 } 11584 11585 // We're not actually writing data to zonepath when validating, and we 11586 // don't actually have a zonepath created yet, so add a key so that the 11587 // payload passes validation 11588 if (!payload.hasOwnProperty('zonepath')) { 11589 toValidate.provisioning.zonepath = true; 11590 } 11591 11592 log.debug({ 11593 firewall: toValidate.firewall, 11594 provisioning: toValidate.provisioning, 11595 payload: payload 11596 }, 'Validating firewall payload'); 11597 11598 firewallVMrun(toValidate, fw.validatePayload, log, 11599 function (err, res) { 11600 if (err) { 11601 log.error(err, 'Error validating firewall payload'); 11602 err.message = 'Invalid firewall payload: ' + err.message; 11603 } 11604 11605 callback(err, res); 11606 return; 11607 }); 11608 } 11609 11610 function addFirewallData(payload, vmobj, log, callback) 11611 { 11612 var firewallOpts = payload.firewall; 11613 11614 assert(log, 'no logger passed to addFirewallData()'); 11615 11616 if (!payload.hasOwnProperty('firewall')) { 11617 firewallOpts = {}; 11618 } 11619 firewallOpts.localVMs = [vmobj]; 11620 11621 log.debug(firewallOpts, 'Adding firewall data'); 11622 firewallVMrun(firewallOpts, fw.add, log, function (err, res) { 11623 if (err) { 11624 log.error(err, 'Error adding firewall data'); 11625 } 11626 11627 callback(err, res); 11628 return; 11629 }); 11630 } 11631 11632 function updateFirewallData(payload, vmobj, log, callback) 11633 { 11634 var enablePrefix = 'En'; 11635 var enableFn = fw.enable; 11636 var firewallOpts = payload.firewall; 11637 11638 assert(log, 'no logger passed to updateFirewallData()'); 11639 11640 if (!payload.hasOwnProperty('firewall')) { 11641 firewallOpts = {}; 11642 } 11643 firewallOpts.localVMs = [vmobj]; 11644 11645 log.debug(firewallOpts, 'Updating firewall data'); 11646 firewallVMrun(firewallOpts, fw.update, log, function (err, res) { 11647 if (err) { 11648 log.error(err, 'Error updating firewall data'); 11649 } 11650 11651 if (!payload.hasOwnProperty('firewall_enabled')) { 11652 callback(err, res); 11653 return; 11654 } 11655 11656 if (!payload.firewall_enabled) { 11657 enableFn = fw.disable; 11658 enablePrefix = 'Dis'; 11659 } 11660 11661 log.debug('%sabling firewall for VM %s', enablePrefix, vmobj.uuid); 11662 firewallVMrun({ vm: vmobj }, enableFn, log, function (err2, res2) { 11663 if (err2) { 11664 log.error(err, 'Error %sabling firewall', 11665 enablePrefix.toLowerCase()); 11666 } 11667 11668 callback(err2, res2); 11669 return; 11670 }); 11671 }); 11672 } 11673 11674 function restartMetadataService(vmobj, payload, log, callback) { 11675 var args; 11676 11677 assert(log, 'no logger passed to restartMetadataService()'); 11678 11679 if (!BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 11680 || !BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features') 11681 || !BRAND_OPTIONS[vmobj.brand].features.mdata_restart) { 11682 log.debug('restarting mdata:fetch service not supported for brand ' 11683 + vmobj.brand); 11684 callback(); 11685 return; 11686 } 11687 11688 if (vmobj.state !== 'running' || !payload.hasOwnProperty('resolvers') 11689 && !payload.hasOwnProperty('routes') 11690 && !payload.hasOwnProperty('set_routes') 11691 && !payload.hasOwnProperty('remove_routes')) { 11692 callback(); 11693 return; 11694 } 11695 11696 log.debug('restarting metadata service for: ' + vmobj.uuid); 11697 11698 args = [vmobj.zonename, '/usr/sbin/svcadm', 'restart', 11699 'svc:/smartdc/mdata:fetch']; 11700 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 11701 execFile('/usr/sbin/zlogin', args, function (err, svc_stdout, svc_stderr) { 11702 if (err) { 11703 log.error({'err': err, 'stdout': svc_stdout, 11704 'stderr': svc_stderr}, 'zlogin for ' + vmobj.zonename 11705 + ' exited with code' + err.code + err.message); 11706 // error here is not fatal as this should be fixed on reboot 11707 } 11708 11709 callback(); 11710 }); 11711 } 11712 11713 function applyUpdates(oldobj, newobj, payload, log, callback) 11714 { 11715 var changed_datasets = false; 11716 11717 assert(log, 'no logger passed to applyUpdates()'); 11718 11719 // Note: oldobj is the VM *before* the update, newobj *after* 11720 log.debug('applying updates to ' + oldobj.uuid); 11721 11722 async.series([ 11723 function (cb) { 11724 if (payload.hasOwnProperty('update_disks') 11725 && oldobj.hasOwnProperty('disks')) { 11726 11727 resizeDisks(oldobj.disks, payload.update_disks, log, 11728 function (err, resized) { 11729 // If any were resized, mark that we changed something 11730 if (!err && resized > 0) { 11731 changed_datasets = true; 11732 } 11733 cb(err); 11734 } 11735 ); 11736 } else { 11737 cb(); 11738 } 11739 }, function (cb) { 11740 if (payload.hasOwnProperty('quota') 11741 && (Number(payload.quota) !== Number(oldobj.quota))) { 11742 11743 setQuota(newobj.zfs_filesystem, payload.quota, log, 11744 function (err) { 11745 11746 if (!err) { 11747 changed_datasets = true; 11748 } 11749 cb(err); 11750 }); 11751 } else { 11752 cb(); 11753 } 11754 }, function (cb) { 11755 // NOTE: we've already validated the value 11756 if (payload.hasOwnProperty('zfs_root_recsize') 11757 && (payload.zfs_root_recsize !== oldobj.zfs_root_recsize)) { 11758 11759 zfs(['set', 'recsize=' + payload.zfs_root_recsize, 11760 newobj.zfs_filesystem], log, function (err, fds) { 11761 11762 if (err) { 11763 log.error(err, 'failed to apply zfs_root_recsize: ' 11764 + fds.stderr); 11765 cb(new Error(rtrim(fds.stderr))); 11766 } else { 11767 cb(); 11768 } 11769 }); 11770 } else { 11771 cb(); 11772 } 11773 }, function (cb) { 11774 // NOTE: we've already validated the value. 11775 if (payload.hasOwnProperty('zfs_data_recsize') 11776 && oldobj.hasOwnProperty('zfs_data_recsize') 11777 && newobj.hasOwnProperty('datasets') 11778 && (newobj.datasets.indexOf(newobj.zfs_filesystem 11779 + '/data') !== -1)) { 11780 11781 zfs(['set', 'recsize=' + payload.zfs_data_recsize, 11782 newobj.zfs_filesystem + '/data'], log, function (err, fds) { 11783 11784 if (err) { 11785 log.error(err, 'failed to apply zfs_data_recsize: ' 11786 + 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_root_compression') 11798 && (payload.zfs_root_compression !== 11799 oldobj.zfs_root_compression)) { 11800 11801 zfs(['set', 'compression=' + payload.zfs_root_compression, 11802 newobj.zfs_filesystem], log, function (err, fds) { 11803 11804 if (err) { 11805 log.error(err, 'failed to apply ' 11806 + 'zfs_root_compression: ' + fds.stderr); 11807 cb(new Error(rtrim(fds.stderr))); 11808 } else { 11809 cb(); 11810 } 11811 }); 11812 } else { 11813 cb(); 11814 } 11815 }, function (cb) { 11816 // NOTE: we've already validated the value 11817 if (payload.hasOwnProperty('zfs_data_compression') 11818 && newobj.hasOwnProperty('datasets') 11819 && (newobj.datasets.indexOf(newobj.zfs_filesystem 11820 + '/data') !== -1)) { 11821 11822 zfs(['set', 'compression=' + payload.zfs_data_compression, 11823 newobj.zfs_filesystem + '/data'], log, function (err, fds) { 11824 11825 if (err) { 11826 log.error(err, 'failed to apply ' 11827 + 'zfs_data_compression: ' + fds.stderr); 11828 cb(new Error(rtrim(fds.stderr))); 11829 } else { 11830 cb(); 11831 } 11832 }); 11833 } else { 11834 cb(); 11835 } 11836 }, function (cb) { 11837 var d; 11838 var disk; 11839 var zfs_updates = []; 11840 11841 if (payload.hasOwnProperty('update_disks')) { 11842 // loop through the disks we updated and perform any updates. 11843 for (disk in payload.update_disks) { 11844 disk = payload.update_disks[disk]; 11845 11846 if (!disk) { 11847 continue; 11848 } 11849 11850 for (d in oldobj.disks) { 11851 d = oldobj.disks[d]; 11852 if (d.path === disk.path 11853 && d.hasOwnProperty('zfs_filesystem')) { 11854 11855 if (disk.hasOwnProperty('compression')) { 11856 zfs_updates.push({ 11857 zfs_filesystem: d.zfs_filesystem, 11858 property: 'compression', 11859 value: disk.compression 11860 }); 11861 } 11862 11863 if (disk.hasOwnProperty('refreservation')) { 11864 zfs_updates.push({ 11865 zfs_filesystem: d.zfs_filesystem, 11866 property: 'refreservation', 11867 value: disk.refreservation + 'M' 11868 }); 11869 } 11870 } 11871 } 11872 } 11873 if (zfs_updates.length > 0) { 11874 log.debug('applying ' + zfs_updates.length 11875 + ' zfs updates'); 11876 async.each(zfs_updates, function (props, f_cb) { 11877 zfs(['set', props.property + '=' + props.value, 11878 props.zfs_filesystem], log, function (err, fds) { 11879 11880 if (err) { 11881 log.error(err, 'failed to set ' + props.property 11882 + '=' + props.value + ' for ' 11883 + props.zfs_filesystem); 11884 } 11885 f_cb(err); 11886 }); 11887 }, function (err) { 11888 log.debug({err: err}, 'end of zfs updates'); 11889 cb(err); 11890 }); 11891 } else { 11892 log.debug('no zfs updates to apply'); 11893 cb(); 11894 } 11895 } else { 11896 cb(); 11897 } 11898 }, function (cb) { 11899 var factor; 11900 var keys = []; 11901 var rctl; 11902 var rctls = { 11903 'cpu_shares': ['zone.cpu-shares'], 11904 'zfs_io_priority': ['zone.zfs-io-priority'], 11905 'max_lwps': ['zone.max-lwps'], 11906 'max_physical_memory': ['zone.max-physical-memory', 11907 (1024 * 1024)], 11908 'max_locked_memory': ['zone.max-locked-memory', (1024 * 1024)], 11909 'max_swap': ['zone.max-swap', (1024 * 1024)], 11910 'cpu_cap': ['zone.cpu-cap'] 11911 }; 11912 11913 if (!BRAND_OPTIONS[oldobj.brand].features.update_rctls) { 11914 cb(); 11915 return; 11916 } 11917 11918 for (rctl in rctls) { 11919 keys.push(rctl); 11920 } 11921 11922 async.forEachSeries(keys, function (prop, c) { 11923 rctl = rctls[prop][0]; 11924 if (rctls[prop][1]) { 11925 factor = rctls[prop][1]; 11926 } else { 11927 factor = 1; 11928 } 11929 11930 if (payload.hasOwnProperty(prop)) { 11931 setRctl(newobj.zonename, rctl, 11932 Number(payload[prop]) * factor, log, 11933 function (err) { 11934 if (err) { 11935 log.warn(err, 'failed to set rctl: ' + prop); 11936 } 11937 c(); 11938 } 11939 ); 11940 } else { 11941 c(); 11942 } 11943 }, function (err) { 11944 cb(err); 11945 }); 11946 }, function (cb) { 11947 if ((payload.hasOwnProperty('vnc_password') 11948 && (oldobj.vnc_password !== newobj.vnc_password)) 11949 || (payload.hasOwnProperty('vnc_port') 11950 && (oldobj.vnc_port !== newobj.vnc_port))) { 11951 11952 // tell vmadmd to refresh_password and port (will restart 11953 // listener) 11954 postVmadmd(newobj.uuid, 'reload_display', {}, log, 11955 function (e) { 11956 11957 if (e) { 11958 cb(new Error('Unable to tell vmadmd to reload VNC: ' 11959 + e.message)); 11960 } else { 11961 cb(); 11962 } 11963 }); 11964 } else if ((payload.hasOwnProperty('spice_password') 11965 && (oldobj.spice_password !== newobj.spice_password)) 11966 || (payload.hasOwnProperty('spice_port') 11967 && (oldobj.spice_port !== newobj.spice_port))) { 11968 11969 // tell vmadmd to refresh_password and port (will restart 11970 // listener) 11971 postVmadmd(newobj.uuid, 'reload_display', {}, log, 11972 function (e) { 11973 11974 if (e) { 11975 cb(new Error('Unable to tell vmadmd to reload SPICE: ' 11976 + e.message)); 11977 } else { 11978 cb(); 11979 } 11980 }); 11981 } else { 11982 cb(); 11983 } 11984 }, function (cb) { 11985 // we do this last, since we need the memory in the zone updated 11986 // first if we're growing this. 11987 if (payload.hasOwnProperty('tmpfs')) { 11988 resizeTmp(newobj.zonename, payload.tmpfs, log, cb); 11989 } else { 11990 cb(); 11991 } 11992 }, function (cb) { 11993 var now = new Date(); 11994 11995 // If we changed any dataset properties, we touch the zone's xml 11996 // file so that last_modified is correct. 11997 if (changed_datasets && newobj.hasOwnProperty('zonename')) { 11998 fs.utimes('/etc/zones/' + newobj.zonename + '.xml', now, now, 11999 function (err) { 12000 if (err) { 12001 log.warn(err, 'Unable to "touch" xml file for "' 12002 + newobj.zonename + '": ' + err.message); 12003 } else { 12004 log.debug('Touched ' + newobj.zonename 12005 + '.xml after datasets were modified.'); 12006 } 12007 // We don't error out if we just couldn't touch because 12008 // the actual updates above already did happen. 12009 cb(); 12010 } 12011 ); 12012 } else { 12013 cb(); 12014 } 12015 } 12016 12017 ], function (err, res) { 12018 log.debug('done applying updates to ' + oldobj.uuid); 12019 callback(err); 12020 }); 12021 } 12022 12023 exports.update = function (uuid, payload, options, callback) 12024 { 12025 var log; 12026 var new_vmobj; 12027 var vmobj; 12028 var unlock; 12029 var lockpath; 12030 12031 // options parameter is optional 12032 if (arguments.length === 3) { 12033 callback = arguments[2]; 12034 options = {}; 12035 } 12036 12037 ensureLogging(true); 12038 if (options.hasOwnProperty('log')) { 12039 log = options.log; 12040 } else { 12041 log = VM.log.child({action: 'update', vm: uuid}); 12042 } 12043 12044 log.info('Updating VM ' + uuid + ' with initial payload:\n' 12045 + JSON.stringify(payload, null, 2)); 12046 12047 async.series([ 12048 function (cb) { 12049 lockpath = '/var/run/vm.' + uuid + '.config.lockfile'; 12050 log.debug('acquiring lock on ' + lockpath); 12051 lock(lockpath, function (err, _unlock) { 12052 log.debug('acquired lock on ' + lockpath); 12053 if (err) { 12054 cb(err); 12055 return; 12056 } 12057 unlock = _unlock; 12058 cb(); 12059 }); 12060 }, 12061 function (cb) { 12062 // for update we currently always load the whole vmobj since the 12063 // update functions may need to look at bits from the existing VM. 12064 VM.load(uuid, {log: log}, function (err, obj) { 12065 if (err) { 12066 cb(err); 12067 return; 12068 } 12069 vmobj = obj; 12070 cb(); 12071 }); 12072 }, function (cb) { 12073 normalizePayload(payload, vmobj, log, function (e) { 12074 log.debug('Used payload:\n' 12075 + JSON.stringify(payload, null, 2)); 12076 cb(e); 12077 }); 12078 }, function (cb) { 12079 var deletables = []; 12080 var to_remove = []; 12081 var n; 12082 12083 // destroy remove_disks before we add in case we're recreating with 12084 // an existing name. 12085 12086 if (payload.hasOwnProperty('remove_disks')) { 12087 to_remove = payload.remove_disks; 12088 for (n in vmobj.disks) { 12089 if (to_remove.indexOf(vmobj.disks[n].path) !== -1) { 12090 deletables.push(vmobj.disks[n]); 12091 } 12092 } 12093 } else { 12094 // no disks to remove so all done. 12095 cb(); 12096 return; 12097 } 12098 12099 function loggedDeleteVolume(volume, callbk) { 12100 return deleteVolume(volume, log, callbk); 12101 } 12102 12103 async.forEachSeries(deletables, loggedDeleteVolume, 12104 function (err) { 12105 if (err) { 12106 log.error(err, 'Unknown error deleting volumes: ' 12107 + err.message); 12108 cb(err); 12109 } else { 12110 log.info('successfully deleted volumes'); 12111 cb(); 12112 } 12113 } 12114 ); 12115 }, function (cb) { 12116 var disks = []; 12117 var matches; 12118 var n; 12119 var p; 12120 var used_disk_indexes = []; 12121 12122 // create any new volumes we need. 12123 if (payload.hasOwnProperty('add_disks')) { 12124 disks = payload.add_disks; 12125 } 12126 12127 // create a list of used indexes so we can find the free ones to 12128 // use in createVolume() 12129 if (vmobj.hasOwnProperty('disks')) { 12130 for (n in vmobj.disks) { 12131 matches = vmobj.disks[n].path.match(/^.*-disk(\d+)$/); 12132 if (matches) { 12133 used_disk_indexes.push(Number(matches[1])); 12134 } 12135 } 12136 } 12137 12138 // add the bits of payload createVolumes() needs. 12139 p = {'add_disks': disks}; 12140 p.uuid = uuid; 12141 if (vmobj.hasOwnProperty('zpool')) { 12142 p.zpool = vmobj.zpool; 12143 } 12144 p.used_disk_indexes = used_disk_indexes; 12145 createVolumes(p, log, function (e) { 12146 cb(e); 12147 }); 12148 }, function (cb) { 12149 updateMetadata(vmobj, payload, log, function (e) { 12150 cb(e); 12151 }); 12152 }, function (cb) { 12153 updateRoutes(vmobj, payload, log, function (e) { 12154 cb(e); 12155 }); 12156 }, function (cb) { 12157 var zcfg; 12158 // generate a payload and send as a file to zonecfg to update 12159 // the zone. 12160 zcfg = buildZonecfgUpdate(vmobj, payload, log); 12161 zonecfgFile(zcfg, ['-u', uuid], log, function (e, fds) { 12162 if (e) { 12163 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12164 'unable to update zonecfg'); 12165 } else { 12166 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12167 'updated zonecfg'); 12168 } 12169 cb(e); 12170 }); 12171 }, function (cb) { 12172 restartMetadataService(vmobj, payload, log, function (e) { 12173 cb(e); 12174 }); 12175 }, function (cb) { 12176 updateVnicProperties(uuid, vmobj, payload, log, function (e) { 12177 cb(e); 12178 }); 12179 }, function (cb) { 12180 // Update the firewall data 12181 updateFirewallData(payload, vmobj, log, cb); 12182 }, function (cb) { 12183 // Do another full reload (all fields) so we can compare in 12184 // applyUpdates() and decide what's changed that we need to apply. 12185 VM.load(uuid, {log: log}, function (e, newobj) { 12186 if (e) { 12187 cb(e); 12188 } else { 12189 new_vmobj = newobj; 12190 cb(); 12191 } 12192 }); 12193 }, function (cb) { 12194 applyUpdates(vmobj, new_vmobj, payload, log, function () { 12195 cb(); 12196 }); 12197 } 12198 ], function (e) { 12199 // If we were able to hold the lockfile, and thus have an unlock 12200 // callback, we must call it before returning, whether or not 12201 // there was an error. 12202 if (unlock) { 12203 log.debug('releasing lock on ' + lockpath); 12204 unlock(function (unlock_err) { 12205 if (unlock_err) { 12206 log.error(err, 'unlock error! (path ' + lockpath + ')'); 12207 } else { 12208 log.debug('released lock on ' + lockpath); 12209 } 12210 callback(e); 12211 }); 12212 } else { 12213 callback(e); 12214 } 12215 }); 12216 }; 12217 12218 function kill(uuid, log, callback) 12219 { 12220 var load_fields; 12221 var unset_autoboot = 'set autoboot=false'; 12222 12223 assert(log, 'no logger passed to kill()'); 12224 12225 log.info('Killing VM ' + uuid); 12226 12227 load_fields = [ 12228 'brand', 12229 'state', 12230 'transition_to', 12231 'uuid' 12232 ]; 12233 12234 /* We load here to ensure this vm exists. */ 12235 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12236 if (err) { 12237 callback(err); 12238 return; 12239 } 12240 12241 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12242 unset_autoboot = 12243 'select attr name=vm-autoboot; set value=false; end'; 12244 } 12245 12246 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) { 12247 var msg = trim(fds.stderr); 12248 12249 if (msg.match(/zone is already halted$/)) { 12250 // remove transition marker, since vm is not running now. 12251 VM.unsetTransition(vmobj, {log: log}, function () { 12252 var new_err; 12253 12254 new_err = new Error('VM ' + vmobj.uuid + ' is already ' 12255 + 'not \'running\' (currently: ' + vmobj.state + ')'); 12256 new_err.code = 'ENOTRUNNING'; 12257 callback(new_err); 12258 }); 12259 } else if (e) { 12260 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12261 'failed to halt VM ' + uuid); 12262 callback(err, msg); 12263 } else { 12264 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12265 'zoneadm halted VM ' + uuid); 12266 zonecfg(['-u', uuid, unset_autoboot], log, 12267 function (error, unset_fds) { 12268 12269 if (error) { 12270 // The vm is dead at this point, erroring out here would 12271 // do no good, so we just log it. 12272 log.error({err: error, stdout: unset_fds.stdout, 12273 stderr: unset_fds.stderr}, 'killVM(): Failed to ' 12274 + unset_autoboot); 12275 } else { 12276 log.debug({stdout: unset_fds.stdout, 12277 stderr: unset_fds.stderr}, 'unset autoboot flag'); 12278 } 12279 if (vmobj.state === 'stopping') { 12280 // remove transition marker 12281 VM.unsetTransition(vmobj, {log: log}, function () { 12282 callback(null, msg); 12283 }); 12284 } else { 12285 callback(null, msg); 12286 } 12287 }); 12288 } 12289 }); 12290 }); 12291 } 12292 12293 function postVmadmd(uuid, action, args, log, callback) 12294 { 12295 var arg; 12296 var url_path = '/vm/' + uuid + '?action=' + action; 12297 var req; 12298 12299 assert(log, 'no logger passed to postVmadmd()'); 12300 12301 if (args) { 12302 for (arg in args) { 12303 if (args.hasOwnProperty(arg)) { 12304 url_path = url_path + '&' + arg + '=' + args[arg]; 12305 } 12306 } 12307 } 12308 12309 log.debug('HTTP POST ' + url_path); 12310 req = http.request( 12311 { method: 'POST', host: '127.0.0.1', port: '8080', path: url_path }, 12312 function (res) { 12313 12314 log.debug('HTTP STATUS: ' + res.statusCode); 12315 log.debug('HTTP HEADERS: ' + JSON.stringify(res.headers)); 12316 res.setEncoding('utf8'); 12317 res.on('data', function (chunk) { 12318 log.debug('HTTP BODY: ' + chunk); 12319 }); 12320 res.on('end', function () { 12321 log.debug('HTTP conversation has completed.'); 12322 callback(); 12323 }); 12324 } 12325 ); 12326 req.on('error', function (e) { 12327 log.error(e, 'HTTP error: ' + e.message); 12328 callback(e); 12329 }); 12330 req.end(); 12331 } 12332 12333 // options parameter is *REQUIRED* for VM.stop() 12334 exports.stop = function (uuid, options, callback) 12335 { 12336 var load_fields; 12337 var log; 12338 var unset_autoboot = 'set autoboot=false'; 12339 var vmobj; 12340 12341 load_fields = [ 12342 'brand', 12343 'state', 12344 'uuid', 12345 'zonename' 12346 ]; 12347 12348 if (!options) { 12349 options = {}; 12350 } 12351 12352 if (options.hasOwnProperty('force') && options.force) { 12353 ensureLogging(true); 12354 if (options.hasOwnProperty('log')) { 12355 log = options.log; 12356 } else { 12357 log = VM.log.child({action: 'stop-F', vm: uuid}); 12358 } 12359 kill(uuid, log, callback); 12360 return; 12361 } else { 12362 ensureLogging(true); 12363 if (options.hasOwnProperty('log')) { 12364 log = options.log; 12365 } else { 12366 log = VM.log.child({action: 'stop', vm: uuid}); 12367 } 12368 } 12369 12370 log.info('Stopping VM ' + uuid); 12371 12372 if (!options.timeout) { 12373 options.timeout = 180; 12374 } 12375 if (!options.transition_to) { 12376 options.transition_to = 'stopped'; 12377 } 12378 12379 async.series([ 12380 function (cb) { 12381 /* We load here to ensure this vm exists. */ 12382 VM.load(uuid, {log: log, fields: load_fields}, function (err, obj) { 12383 var new_err; 12384 12385 if (err) { 12386 log.error(err); 12387 cb(err); 12388 return; 12389 } else { 12390 vmobj = obj; 12391 if (vmobj.state !== 'running') { 12392 new_err = new Error('VM ' + vmobj.uuid + ' is already ' 12393 + 'not \'running\' (currently: ' + vmobj.state 12394 + ')'); 12395 new_err.code = 'ENOTRUNNING'; 12396 cb(new_err); 12397 } else { 12398 cb(); 12399 } 12400 } 12401 }); 12402 }, function (cb) { 12403 // When stopping a VM that uses vm_autoboot, we assume we also do 12404 // the stop through vmadmd. 12405 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12406 async.series([ 12407 function (callbk) { 12408 setTransition(vmobj, 'stopping', options.transition_to, 12409 (options.timeout * 1000), log, function (err) { 12410 12411 callbk(err); 12412 }); 12413 }, function (callbk) { 12414 postVmadmd(vmobj.uuid, 'stop', 12415 {'timeout': options.timeout}, log, function (err) { 12416 12417 if (err) { 12418 log.error(err); 12419 err.message = 'Unable to post "stop" to vmadmd:' 12420 + ' ' + err.message; 12421 } 12422 callbk(err); 12423 }); 12424 }, function (callbk) { 12425 12426 // different version for VMs 12427 unset_autoboot = 'select attr name=vm-autoboot; ' 12428 + 'set value=false; end'; 12429 12430 zonecfg(['-u', uuid, unset_autoboot], log, 12431 function (err, fds) { 12432 if (err) { 12433 // The vm is dead at this point, failing 12434 // here would do no good, so we just log it. 12435 log.error({err: err, stdout: fds.stdout, 12436 stderr: fds.stderr}, 'stop(): Failed to' 12437 + ' ' + unset_autoboot + ' for ' + uuid 12438 + ': ' + err.message); 12439 } else { 12440 log.info({stdout: fds.stdout, 12441 stderr: fds.stderr}, 'Stopped ' + uuid); 12442 } 12443 callbk(); 12444 } 12445 ); 12446 } 12447 ], function (err) { 12448 cb(err); 12449 }); 12450 } else { // no vm_autoboot / vmadmd stop 12451 cb(); 12452 } 12453 }, function (cb) { 12454 var args; 12455 12456 // joyent brand specific stuff 12457 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0', 12458 '-i', '5']; 12459 12460 // not using vm_autoboot means using the 'normal' boot process 12461 if (!BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) { 12462 async.series([ 12463 function (callbk) { 12464 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 12465 execFile('/usr/sbin/zlogin', args, 12466 function (err, stdout, stderr) { 12467 12468 if (err) { 12469 log.error({'err': err, 'stdout': stdout, 12470 'stderr': stderr}, 'zlogin for ' 12471 + vmobj.zonename + ' exited with code' 12472 + err.code + ': ' + err.message); 12473 callbk(err); 12474 } else { 12475 log.debug('zlogin claims to have worked, ' 12476 + 'stdout:\n' + stdout + '\nstderr:\n' 12477 + stderr); 12478 callbk(); 12479 } 12480 }); 12481 }, function (callbk) { 12482 zonecfg(['-u', uuid, unset_autoboot], log, 12483 function (err, fds) { 12484 if (err) { 12485 // The vm is dead at this point, failing 12486 // do no good, so we just log it. 12487 log.warn({err: err, stdout: fds.stdout, 12488 stderr: fds.stderr}, 'Failed to ' 12489 + unset_autoboot + ' for ' + uuid); 12490 } else { 12491 log.info({stdout: fds.stdout, 12492 stderr: fds.stderr}, 'Stopped ' + uuid); 12493 } 12494 callbk(); 12495 } 12496 ); 12497 } 12498 ], function (err) { 12499 cb(err); 12500 }); 12501 } else { // using vmautoboot so won't shutdown from in the zone 12502 cb(); 12503 } 12504 }, function (cb) { 12505 // Verify it's shut down 12506 VM.waitForZoneState(vmobj, 'installed', {log: log}, 12507 function (err, result) { 12508 12509 if (err) { 12510 if (err.code === 'ETIMEOUT') { 12511 log.info(err, 'timeout waiting for zone to go to ' 12512 + '"installed"'); 12513 } else { 12514 log.error(err, 'unknown error waiting for zone to go' 12515 + ' "installed"'); 12516 } 12517 cb(err); 12518 } else { 12519 // zone got to stopped 12520 log.info('VM seems to have switched to "installed"'); 12521 cb(); 12522 } 12523 }); 12524 } 12525 ], function (err) { 12526 callback(err); 12527 }); 12528 }; 12529 12530 // sends several query-* commands to QMP to get details for a VM 12531 exports.info = function (uuid, types, options, callback) 12532 { 12533 var load_fields; 12534 var log; 12535 12536 // options is optional 12537 if (arguments.length === 3) { 12538 callback = arguments[2]; 12539 options = {}; 12540 } 12541 12542 ensureLogging(false); 12543 if (options.hasOwnProperty('log')) { 12544 log = options.log; 12545 } else { 12546 log = VM.log.child({action: 'info', vm: uuid}); 12547 } 12548 12549 load_fields = [ 12550 'brand', 12551 'state', 12552 'uuid' 12553 ]; 12554 12555 // load to ensure we're a VM 12556 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12557 var type; 12558 12559 if (err) { 12560 callback(err); 12561 return; 12562 } 12563 12564 if (!BRAND_OPTIONS[vmobj.brand].features.runtime_info) { 12565 // XXX if support is added to other brands, update this message. 12566 callback(new Error('the info command is only supported for KVM ' 12567 + 'VMs')); 12568 return; 12569 } 12570 12571 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') { 12572 callback(new Error('Unable to get info for vm from state "' 12573 + vmobj.state + '", must be "running" or "stopping".')); 12574 return; 12575 } 12576 12577 if (!types) { 12578 types = ['all']; 12579 } 12580 12581 for (type in types) { 12582 type = types[type]; 12583 if (VM.INFO_TYPES.indexOf(type) === -1) { 12584 callback(new Error('unknown info type: ' + type)); 12585 return; 12586 } 12587 } 12588 12589 http.get({ host: '127.0.0.1', port: 8080, path: '/vm/' + uuid + '/info' 12590 + '?types=' + types.join(',') }, function (res) { 12591 12592 var data = ''; 12593 12594 if (res.statusCode !== 200) { 12595 callback(new Error('Unable to get info from vmadmd, query ' 12596 + 'returned ' + res.statusCode + '.')); 12597 } else { 12598 res.on('data', function (d) { 12599 data = data + d.toString(); 12600 }); 12601 res.on('end', function (d) { 12602 callback(null, JSON.parse(data)); 12603 }); 12604 } 12605 } 12606 ).on('error', function (e) { 12607 log.error(e); 12608 callback(e); 12609 }); 12610 }); 12611 }; 12612 12613 function reset(uuid, log, callback) 12614 { 12615 var load_fields; 12616 12617 assert(log, 'no logger passed to reset()'); 12618 12619 log.info('Resetting VM ' + uuid); 12620 12621 load_fields = [ 12622 'brand', 12623 'state', 12624 'uuid' 12625 ]; 12626 12627 /* We load here to ensure this vm exists. */ 12628 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12629 if (err) { 12630 callback(err); 12631 return; 12632 } 12633 12634 if (vmobj.state !== 'running') { 12635 callback(new Error('Cannot reset vm from state "' 12636 + vmobj.state + '", must be "running".')); 12637 return; 12638 } 12639 12640 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) { 12641 postVmadmd(vmobj.uuid, 'reset', {}, log, function (e) { 12642 if (e) { 12643 callback(new Error('Unable to post "reset" to ' 12644 + 'vmadmd: ' + e.message)); 12645 } else { 12646 callback(); 12647 } 12648 }); 12649 } else { 12650 zoneadm(['-u', vmobj.uuid, 'reboot', '-X'], log, function (e, fds) { 12651 if (e) { 12652 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr}, 12653 'zoneadm failed to reboot VM ' + vmobj.uuid); 12654 callback(new Error(rtrim(fds.stderr))); 12655 } else { 12656 log.debug({stdout: fds.stdout, stderr: fds.stderr}, 12657 'zoneadm rebooted VM ' + vmobj.uuid); 12658 callback(); 12659 } 12660 }); 12661 } 12662 }); 12663 } 12664 12665 // options is *REQUIRED* for VM.reboot() 12666 exports.reboot = function (uuid, options, callback) 12667 { 12668 var cleanup; 12669 var log; 12670 var reboot_async = false; 12671 var reboot_complete = false; 12672 var vmobj; 12673 12674 if (options.hasOwnProperty('log')) { 12675 log = options.log; 12676 } 12677 12678 if (options.hasOwnProperty('force') && options.force) { 12679 ensureLogging(true); 12680 if (!log) { 12681 log = VM.log.child({action: 'reboot-F', vm: uuid}); 12682 } 12683 reset(uuid, log, callback); 12684 return; 12685 } else { 12686 ensureLogging(true); 12687 log = VM.log.child({action: 'reboot', vm: uuid}); 12688 } 12689 12690 log.info('Rebooting VM ' + uuid); 12691 12692 if (!options) { 12693 options = {}; 12694 } 12695 12696 async.series([ 12697 function (cb) { 12698 var load_fields = [ 12699 'brand', 12700 'nics', 12701 'state', 12702 'zonename' 12703 ]; 12704 12705 VM.load(uuid, {fields: load_fields, log: log}, 12706 function (err, obj) { 12707 12708 if (err) { 12709 cb(err); 12710 return; 12711 } 12712 12713 if (obj.state !== 'running') { 12714 cb(new Error('Cannot reboot vm from state "' + obj.state 12715 + '", must be "running"')); 12716 return; 12717 } 12718 12719 vmobj = obj; 12720 cb(); 12721 }); 12722 }, function (cb) { 12723 // If nic tags have disappeared out from under us, don't allow a 12724 // reboot that will put us into a bad state 12725 lookupInvalidNicTags(vmobj.nics, log, function (e) { 12726 if (e) { 12727 cb(new Error('Cannot reboot vm: ' + e.message)); 12728 return; 12729 } 12730 12731 cb(); 12732 }); 12733 12734 }, function (cb) { 12735 var watcherobj; 12736 12737 if (!reboot_async) { 12738 watcherobj = watchZoneTransitions(function (err, ze) { 12739 if (!err && ze.zonename !== vmobj.zonename) { 12740 // not something we need to handle 12741 return; 12742 } 12743 12744 if (err) { 12745 // XXX what should we do here? 12746 log.error(err); 12747 return; 12748 } 12749 12750 log.debug(ze); // TODO move to trace 12751 12752 if (ze.newstate === 'running' 12753 && ze.oldstate !== 'running') { 12754 12755 if (watcherobj) { 12756 // cleanup our watcher since we found what we're 12757 // looking for. 12758 cleanup(); 12759 } 12760 12761 reboot_complete = true; 12762 } 12763 }, log); 12764 cleanup = watcherobj.cleanup; 12765 } 12766 12767 cb(); 12768 }, function (cb) { 12769 var args; 12770 12771 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) { 12772 // here we stop the machine and set a transition so vmadmd will 12773 // start the machine once the stop finished. 12774 options.transition_to = 'start'; 12775 options.log = log; 12776 VM.stop(uuid, options, function (err) { 12777 if (err) { 12778 cb(err); 12779 } else { 12780 cb(); 12781 } 12782 }); 12783 } else { 12784 // joyent branded zones 12785 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0', 12786 '-i', '6']; 12787 log.debug('/usr/sbin/zlogin ' + args.join(' ')); 12788 execFile('/usr/sbin/zlogin', args, 12789 function (err, stdout, stderr) { 12790 if (err) { 12791 log.error({'err': err, 'stdout': stdout, 12792 'stderr': stderr}, 'zlogin for ' + vmobj.zonename 12793 + ' exited with code' + err.code + ': ' 12794 + err.message); 12795 cb(err); 12796 } else { 12797 cb(); 12798 } 12799 }); 12800 } 12801 }, function (cb) { 12802 var ival; 12803 var ticks = 0; 12804 12805 if (reboot_async) { 12806 cb(); 12807 return; 12808 } else { 12809 ticks = 180 * 10; // (180 * 10) 100ms ticks = 3m 12810 ival = setInterval(function () { 12811 if (reboot_complete) { 12812 log.debug('reboot marked complete, cleaning up'); 12813 clearInterval(ival); 12814 cleanup(); 12815 cb(); 12816 return; 12817 } 12818 ticks--; 12819 if (ticks <= 0) { 12820 // timed out 12821 log.debug('reboot timed out, cleaning up'); 12822 clearInterval(ival); 12823 cleanup(); 12824 cb(new Error('timed out waiting for zone to reboot')); 12825 return; 12826 } 12827 }, 100); 12828 } 12829 } 12830 ], function (err) { 12831 callback(err); 12832 }); 12833 }; 12834 12835 // options is *REQUIRED* for VM.sysrq 12836 exports.sysrq = function (uuid, req, options, callback) 12837 { 12838 var load_fields; 12839 var log; 12840 12841 ensureLogging(true); 12842 12843 if (options.hasOwnProperty('log')) { 12844 log = options.log; 12845 } else { 12846 log = VM.log.child({action: 'sysrq-' + req, vm: uuid}); 12847 } 12848 12849 log.info('Sending sysrq "' + req + '" to ' + uuid); 12850 12851 load_fields = [ 12852 'brand', 12853 'state', 12854 'uuid' 12855 ]; 12856 12857 /* We load here to ensure this vm exists. */ 12858 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12859 if (err) { 12860 callback(err); 12861 return; 12862 } 12863 12864 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') { 12865 callback(new Error('Unable to send request to vm from state "' 12866 + vmobj.state + '", must be "running" or "stopping".')); 12867 return; 12868 } 12869 12870 if (BRAND_OPTIONS[vmobj.brand].features.type !== 'KVM') { 12871 callback(new Error('The sysrq command is only supported for KVM.')); 12872 return; 12873 } 12874 12875 if (VM.SYSRQ_TYPES.indexOf(req) === -1) { 12876 callback(new Error('Invalid sysrq "' + req + '" valid values: ' 12877 + '"' + VM.SYSRQ_TYPES.join('","') + '".')); 12878 return; 12879 } 12880 12881 postVmadmd(vmobj.uuid, 'sysrq', {'request': req}, log, function (e) { 12882 if (e) { 12883 callback(new Error('Unable to post "sysrq" to vmadmd: ' 12884 + e.message)); 12885 } else { 12886 callback(); 12887 } 12888 }); 12889 }); 12890 }; 12891 12892 exports.console = function (uuid, options, callback) 12893 { 12894 var load_fields; 12895 var log; 12896 12897 // options is optional 12898 if (arguments.length === 2) { 12899 callback = arguments[1]; 12900 options = {}; 12901 } 12902 12903 ensureLogging(false); 12904 if (options.hasOwnProperty('log')) { 12905 log = options.log; 12906 } else { 12907 log = VM.log.child({action: 'console', vm: uuid}); 12908 } 12909 12910 load_fields = [ 12911 'brand', 12912 'state', 12913 'zonename', 12914 'zonepath' 12915 ]; 12916 12917 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) { 12918 var args; 12919 var child; 12920 var cmd; 12921 var stty; 12922 12923 if (err) { 12924 callback(err); 12925 return; 12926 } 12927 if (vmobj.state !== 'running') { 12928 callback(new Error('cannot connect to console when state is ' 12929 + '"' + vmobj.state + '" must be "running".')); 12930 return; 12931 } 12932 12933 if (BRAND_OPTIONS[vmobj.brand].features.zlogin_console) { 12934 cmd = '/usr/sbin/zlogin'; 12935 args = ['-C', '-e', '\\035', vmobj.zonename]; 12936 12937 log.debug(cmd + ' ' + args.join(' ')); 12938 child = spawn(cmd, args, {customFds: [0, 1, 2]}); 12939 child.on('close', function (code) { 12940 log.debug('zlogin process exited with code ' + code); 12941 callback(); 12942 }); 12943 } else if (BRAND_OPTIONS[vmobj.brand].features.serial_console) { 12944 async.series([ 12945 function (cb) { 12946 cmd = '/usr/bin/stty'; 12947 args = ['-g']; 12948 stty = ''; 12949 12950 log.debug(cmd + ' ' + args.join(' ')); 12951 child = spawn(cmd, args, {customFds: [0, -1, -1]}); 12952 child.stdout.on('data', function (data) { 12953 // log.debug('data: ' + data.toString()); 12954 stty = data.toString(); 12955 }); 12956 child.on('close', function (code) { 12957 log.debug('stty process exited with code ' + code); 12958 cb(); 12959 }); 12960 }, function (cb) { 12961 cmd = '/usr/bin/socat'; 12962 args = ['unix-client:' + vmobj.zonepath 12963 + '/root/tmp/vm.console', '-,raw,echo=0,escape=0x1d']; 12964 12965 log.debug(cmd + ' ' + args.join(' ')); 12966 child = spawn(cmd, args, {customFds: [0, 1, 2]}); 12967 child.on('close', function (code) { 12968 log.debug('zlogin process exited with code ' + code); 12969 cb(); 12970 }); 12971 }, function (cb) { 12972 cmd = '/usr/bin/stty'; 12973 args = [stty]; 12974 12975 log.debug(cmd + ' ' + args.join(' ')); 12976 child = spawn(cmd, args, {customFds: [0, -1, -1]}); 12977 child.on('close', function (code) { 12978 log.debug('stty process exited with code ' + code); 12979 cb(); 12980 }); 12981 } 12982 ], function (e, results) { 12983 callback(e); 12984 }); 12985 } else { 12986 callback(new Error('Cannot get console for brand: ' + vmobj.brand)); 12987 } 12988 }); 12989 };