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 };