1 /*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license at http://smartos.org/CDDL
10 *
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file.
16 *
17 * If applicable, add the following below this CDDL HEADER, with the
18 * fields enclosed by brackets "[]" replaced with your own identifying
19 * information: Portions Copyright [yyyy] [name of copyright owner]
20 *
21 * CDDL HEADER END
22 *
23 * Copyright (c) 2013, Joyent, Inc. All rights reserved.
24 *
25 * Experimental functions, expect these interfaces to be unstable and
26 * potentially go away entirely:
27 *
28 * create_snapshot(uuid, snapname, options, callback)
29 * delete_snapshot(uuid, snapname, options, callback)
30 * install(uuid, callback)
31 * receive(target, options, callback)
32 * reprovision(uuid, payload, options, callback)
33 * rollback_snapshot(uuid, snapname, options, callback)
34 * send(uuid, where, options, callback)
35 * getSysinfo(args, callback)
36 * validate(brand, action, payload, callback)
37 * waitForZoneState(payload, state, options, callback)
38 *
39 * Exported functions:
40 *
41 * console(uuid, callback)
42 * create(properties, callback)
43 * delete(uuid, callback)
44 * flatten(vmobj, key)
45 * info(uuid, types, callback)
46 * load([zonename|uuid], callback)
47 * lookup(match, callback)
48 * reboot(uuid, options={[force=true]}, callback)
49 * start(uuid, extra, callback)
50 * stop(uuid, options={[force=true]}, callback)
51 * sysrq(uuid, req=[nmi|screenshot], options={}, callback)
52 * update(uuid, properties, callback)
53 *
54 * Exported variables:
55 *
56 * logname - you can set this to a string [a-zA-Z_] to use as log name
57 * logger - you can set this to a node-bunyan log stream to capture the logs
58 * INFO_TYPES - list of supported types for the info command
59 * SYSRQ_TYPES - list of supported requests for sysrq
60 */
61
62 // Ensure we're using the platform's node
63 require('/usr/node/node_modules/platform_node_version').assert();
64
65 var assert = require('assert');
66 var async = require('/usr/node/node_modules/async');
67 var bunyan = require('/usr/node/node_modules/bunyan');
68 var cp = require('child_process');
69 var dladm = require('/usr/vm/node_modules/dladm');
70 var EventEmitter = require('events').EventEmitter;
71 var exec = cp.exec;
72 var execFile = cp.execFile;
73 var expat = require('/usr/node/node_modules/node-expat');
74 var fs = require('fs');
75 var fw = require('/usr/fw/lib/fw');
76 var http = require('http');
77 var net = require('net');
78 var path = require('path');
79 var Qmp = require('/usr/vm/node_modules/qmp').Qmp;
80 var spawn = cp.spawn;
81 var sprintf = require('/usr/node/node_modules/sprintf').sprintf;
82 var tty = require('tty');
83 var util = require('util');
84
85 var log_to_file = false;
86
87 // keep the last 512 messages just in case we end up wanting them.
88 var ringbuffer = new bunyan.RingBuffer({ limit: 512 });
89
90 // zfs_list_queue variables for the serialization of 'zfs list' calls
91 var zfs_list_in_progress = {};
92 var zfs_list_queue;
93
94 // global handle for the zoneevent watcher
95 var zoneevent;
96
97 /*
98 * zone states from libzonecfg/common/zonecfg_impl.h
99 *
100 * #define ZONE_STATE_STR_CONFIGURED "configured"
101 * #define ZONE_STATE_STR_INCOMPLETE "incomplete"
102 * #define ZONE_STATE_STR_INSTALLED "installed"
103 * #define ZONE_STATE_STR_READY "ready"
104 * #define ZONE_STATE_STR_MOUNTED "mounted"
105 * #define ZONE_STATE_STR_RUNNING "running"
106 * #define ZONE_STATE_STR_SHUTTING_DOWN "shutting_down"
107 * #define ZONE_STATE_STR_DOWN "down"
108 *
109 */
110
111 exports.FLATTENABLE_ARRAYS = [
112 'resolvers'
113 ];
114 exports.FLATTENABLE_ARRAY_HASH_KEYS = [
115 'disks',
116 'nics'
117 ];
118 exports.FLATTENABLE_HASH_KEYS = [
119 'customer_metadata',
120 'internal_metadata',
121 'routes',
122 'tags'
123 ];
124
125 var DEFAULT_MDATA_TIMEOUT = 300;
126 var DISABLED = 0;
127 var MAX_SNAPNAME_LENGTH = 64;
128 var MINIMUM_MAX_SWAP = 256;
129 var PROVISION_TIMEOUT = 300;
130 var STOP_TIMEOUT = 60;
131 var VM = this;
132
133 VM.log = null;
134
135 // can be (re)set by loader before we start.
136 exports.logger = null;
137 exports.loglevel = 'debug';
138
139 // OpenOnErrorFileStream is a bunyan stream that only creates the file when
140 // there's an error or higher level message or when the global log_to_file
141 // variable is set. For actions that modify things log_to_file is always set.
142 // For other actions we shouldn't log in the normal case but where we do want
143 // logs when something breaks. Thanks to Trent++ for most of this code.
144 //
145 // Note: if you want to rotate the logs while this is writing to a file, you
146 // can first move it. The watcher will notice that the log file was moved and
147 // reopen a new file with the original name.
148
149 function OpenOnErrorFileStream(filename) {
150 this.path = filename;
151 this.write = this.constructor.prototype.write1;
152 this.end = this.constructor.prototype.end1;
153 this.emit = this.constructor.prototype.emit1;
154 this.once = this.constructor.prototype.once1;
155
156 this.newStream = function () {
157 var self = this;
158 var watcher;
159
160 self.stream = fs.createWriteStream(self.path,
161 {flags: 'a', encoding: 'utf8'});
162
163 watcher = fs.watch(self.path, {persistent: false}, function (evt) {
164 if (evt != 'rename') {
165 return;
166 }
167 // file was renamed, we want to reopen.
168 if (self.stream) {
169 self.stream.destroySoon();
170 }
171 watcher.close();
172 self.stream = null;
173 });
174 };
175 }
176
177 OpenOnErrorFileStream.prototype.end1 = function () {
178 // in initial mode we're not writing anything, so nothing to flush
179 return;
180 };
181
182 OpenOnErrorFileStream.prototype.emit1 = function () {
183 return;
184 };
185
186 // Warning: never emits anything
187 OpenOnErrorFileStream.prototype.once1 = function () {
188 return;
189 };
190
191 // used until first ERROR or higher, then opens file and ensures future writes
192 // go to .write2()
193 OpenOnErrorFileStream.prototype.write1 = function (rec) {
194 var r;
195 var stream;
196
197 if (rec.level >= bunyan.ERROR || log_to_file) {
198 if (! this.stream) {
199 this.newStream();
200 }
201
202 stream = this.stream;
203
204 this.emit = function () { stream.emit.apply(stream, arguments); };
205 this.end = function () { stream.end.apply(stream, arguments); };
206 this.once = function () { stream.once.apply(stream, arguments); };
207 this.write = this.constructor.prototype.write2;
208 // dump out logs from ringbuffer too since there was an error so we can
209 // figure out what's going on.
210 for (r in ringbuffer.records) {
211 r = ringbuffer.records[r];
212 if (r != rec) {
213 this.write(r);
214 }
215 }
216
217 this.write(rec);
218 }
219
220 // This write doesn't fail (since it's going to memory or nowhere) so we
221 // always return true so that callers don't try to wait for 'drain' which
222 // we'll not emit.
223 return true;
224 };
225
226 // used when writing to file
227 OpenOnErrorFileStream.prototype.write2 = function (rec) {
228 var str;
229
230 // need to support writing '' so we know when to drain
231 if (typeof (rec) === 'string' && rec.length < 1) {
232 str = '';
233 } else {
234 str = JSON.stringify(rec, bunyan.safeCycles()) + '\n';
235 }
236
237 if (! this.stream) {
238 this.newStream();
239 }
240
241 return this.stream.write(str);
242 };
243
244 // This function should be called by any exported function from this module.
245 // It ensures that a logger is setup. If side_effects is true, we'll start
246 // writing log messages to the file right away. If not, we'll only start
247 // logging after we hit a message error or higher. This is intended such that
248 // things that are expected to change the state or modify VMs on the system:
249 // eg. create, start, stop, delete should have this set true. It should be
250 // set false when the action should not cause changes to the system:
251 // eg.: load, lookup, info, console, &c.
252 function ensureLogging(side_effects)
253 {
254 side_effects = !!side_effects; // make it boolean (undef === false)
255
256 var filename;
257 var logname;
258 var streams = [];
259
260 function start_logging() {
261 var params = {
262 name: logname,
263 streams: streams,
264 serializers: bunyan.stdSerializers
265 };
266
267 if (process.env.REQ_ID) {
268 params.req_id = process.env.REQ_ID;
269 } else if (process.env.req_id) {
270 params.req_id = process.env.req_id;
271 }
272 VM.log = bunyan.createLogger(params);
273 }
274
275 // This is here in case an app calls a lookup first and then a create. The
276 // logger will get created in no-sideeffects mode for the lookup but when
277 // the create is called this will force the switch to writing.
278 if (side_effects) {
279 log_to_file = true;
280 }
281
282 if (VM.log) {
283 // We're already logging, don't break things.
284 return;
285 }
286
287 if (VM.hasOwnProperty('logname')) {
288 logname = VM.logname.replace(/[^a-zA-Z\_]/g, '');
289 }
290 if (!logname || logname.length < 1) {
291 logname = 'VM';
292 }
293
294 if (VM.hasOwnProperty('logger') && VM.logger) {
295 // Use concat, in case someone's sneaky and makes more than one logger.
296 // We don't officially support that yet though.
297 streams = streams.concat(VM.logger);
298 }
299
300 // Add the ringbuffer which we'll dump if we switch from not writing to
301 // writing, and so that they'll show up in dumps.
302 streams.push({
303 level: 'trace',
304 type: 'raw',
305 stream: ringbuffer
306 });
307
308 try {
309 if (!fs.existsSync('/var/log/vm')) {
310 fs.mkdirSync('/var/log/vm');
311 }
312 if (!fs.existsSync('/var/log/vm/logs')) {
313 fs.mkdirSync('/var/log/vm/logs');
314 }
315 } catch (e) {
316 // We can't ever log to a file in /var/log/vm/logs if we can't create
317 // it, so we just log to ring buffer (above).
318 start_logging();
319 return;
320 }
321
322 filename = '/var/log/vm/logs/' + Date.now(0) + '-'
323 + sprintf('%06d', process.pid) + '-' + logname + '.log';
324
325 streams.push({
326 type: 'raw',
327 stream: new OpenOnErrorFileStream(filename),
328 level: VM.loglevel
329 });
330
331 start_logging();
332 }
333
334 function ltrim(str, chars)
335 {
336 chars = chars || '\\s';
337 str = str || '';
338 return str.replace(new RegExp('^[' + chars + ']+', 'g'), '');
339 }
340
341 function rtrim(str, chars)
342 {
343 chars = chars || '\\s';
344 str = str || '';
345 return str.replace(new RegExp('[' + chars + ']+$', 'g'), '');
346 }
347
348 function trim(str, chars)
349 {
350 return ltrim(rtrim(str, chars), chars);
351 }
352
353 function isUUID(str) {
354 var re = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
355 if (str && str.length === 36 && str.match(re)) {
356 return true;
357 } else {
358 return false;
359 }
360 }
361
362 function fixBoolean(str)
363 {
364 if (str === 'true') {
365 return true;
366 } else if (str === 'false') {
367 return false;
368 } else {
369 return str;
370 }
371 }
372
373 function fixBooleanLoose(str)
374 {
375 if (str === 'true' || str === '1' || str === 1) {
376 return true;
377 } else if (str === 'false' || str === '0' || str === 0) {
378 return false;
379 } else {
380 return str;
381 }
382 }
383
384 function isCIDR(str) {
385 if (typeof (str) !== 'string') {
386 return false;
387 }
388 var parts = str.split('/');
389 if (parts.length !== 2 || !net.isIPv4(parts[0])) {
390 return false;
391 }
392
393 var size = Number(parts[1]);
394 if (!size || size < 8 || size > 32) {
395 return false;
396 }
397
398 return true;
399 }
400
401 // IMPORTANT:
402 //
403 // Some of these properties get translated below into backward compatible
404 // names.
405 //
406
407 var UPDATABLE_NIC_PROPS = [
408 'primary',
409 'nic_tag',
410 'vrrp_vrid',
411 'vrrp_primary_ip',
412 'blocked_outgoing_ports',
413 'mac',
414 'gateway',
415 'ip',
416 'model',
417 'netmask',
418 'network_uuid',
419 'dhcp_server',
420 'allow_dhcp_spoofing',
421 'allow_ip_spoofing',
422 'allow_mac_spoofing',
423 'allow_restricted_traffic',
424 'allow_unfiltered_promisc',
425 'vlan_id'
426 ];
427
428 var UPDATABLE_DISK_PROPS = [
429 'boot',
430 'model'
431 ];
432
433 // Note: this doesn't include 'state' because of 'stopping' which is a virtual
434 // state and therefore lookups would be wrong (because they'd search on real
435 // state).
436 var QUICK_LOOKUP = [
437 'zoneid',
438 'zonename',
439 'zonepath',
440 'uuid',
441 'brand',
442 'ip_type'
443 ];
444
445 exports.DISK_MODELS = [
446 'virtio',
447 'ide',
448 'scsi'
449 ];
450
451 exports.VGA_TYPES = [
452 'cirrus',
453 'std',
454 'vmware',
455 'qxl',
456 'xenfb'
457 ];
458
459 exports.INFO_TYPES = [
460 'all',
461 'block',
462 'blockstats',
463 'chardev',
464 'cpus',
465 'kvm',
466 'pci',
467 'spice',
468 'status',
469 'version',
470 'vnc'
471 ];
472
473 exports.SYSRQ_TYPES = [
474 'nmi',
475 'screenshot'
476 ];
477
478 exports.COMPRESSION_TYPES = [
479 'on',
480 'off',
481 'lzjb',
482 'gzip',
483 'gzip-1',
484 'gzip-2',
485 'gzip-3',
486 'gzip-4',
487 'gzip-5',
488 'gzip-6',
489 'gzip-7',
490 'gzip-8',
491 'gzip-9',
492 'zle'
493 ];
494
495 exports.KVM_MEM_OVERHEAD = 1024;
496 exports.KVM_MIN_MEM_OVERHEAD = 256;
497
498 var XML_PROPERTIES = {
499 'zone': {
500 'name': 'zonename',
501 'zonepath': 'zonepath',
502 'autoboot': 'autoboot',
503 'brand': 'brand',
504 'limitpriv': 'limit_priv',
505 'fs-allowed': 'fs_allowed'
506 },
507 'zone.attr': {
508 'alias': 'alias',
509 'archive-on-delete': 'archive_on_delete',
510 'billing-id': 'billing_id',
511 'boot': 'boot',
512 'cpu-type': 'cpu_type',
513 'create-timestamp': 'create_timestamp',
514 'dataset-uuid': 'image_uuid',
515 'default-gateway': 'default_gateway',
516 'disk-driver': 'disk_driver',
517 'dns-domain': 'dns_domain',
518 'do-not-inventory': 'do_not_inventory',
519 'failed': 'failed',
520 'firewall-enabled': 'firewall_enabled',
521 'hostname': 'hostname',
522 'init-name': 'init_name',
523 'never-booted': 'never_booted',
524 'nic-driver': 'nic_driver',
525 'owner-uuid': 'owner_uuid',
526 'package-name': 'package_name',
527 'package-version': 'package_version',
528 'qemu-extra-opts': 'qemu_extra_opts',
529 'qemu-opts': 'qemu_opts',
530 'ram': 'ram',
531 'restart-init': 'restart_init',
532 'resolvers': 'resolvers',
533 'spice-opts': 'spice_opts',
534 'spice-password': 'spice_password',
535 'spice-port': 'spice_port',
536 'tmpfs': 'tmpfs',
537 'transition': 'transition',
538 'vcpus': 'vcpus',
539 'vga': 'vga',
540 'virtio-txtimer': 'virtio_txtimer',
541 'virtio-txburst': 'virtio_txburst',
542 'vm-version': 'v',
543 'vm-autoboot': 'vm_autoboot',
544 'vnc-password': 'vnc_password',
545 'vnc-port': 'vnc_port'
546 },
547 'zone.rctl.zone.cpu-shares.rctl-value': {
548 'limit': 'cpu_shares'
549 },
550 'zone.rctl.zone.cpu-cap.rctl-value': {
551 'limit': 'cpu_cap'
552 },
553 'zone.rctl.zone.zfs-io-priority.rctl-value': {
554 'limit': 'zfs_io_priority'
555 },
556 'zone.rctl.zone.max-lwps.rctl-value': {
557 'limit': 'max_lwps'
558 },
559 'zone.rctl.zone.max-physical-memory.rctl-value': {
560 'limit': 'max_physical_memory'
561 },
562 'zone.rctl.zone.max-locked-memory.rctl-value': {
563 'limit': 'max_locked_memory'
564 },
565 'zone.rctl.zone.max-swap.rctl-value': {
566 'limit': 'max_swap'
567 },
568 'nic': {
569 'ip': 'ip',
570 'mac-addr': 'mac',
571 'physical': 'interface',
572 'vlan-id': 'vlan_id',
573 'global-nic': 'nic_tag',
574 'dhcp_server': 'dhcp_server',
575 'allow_dhcp_spoofing': 'allow_dhcp_spoofing',
576 'allow_ip_spoofing': 'allow_ip_spoofing',
577 'allow_mac_spoofing': 'allow_mac_spoofing',
578 'allow_restricted_traffic': 'allow_restricted_traffic',
579 'allow_unfiltered_promisc': 'allow_unfiltered_promisc',
580 'allowed_ips': 'allowed_ips',
581 'netmask': 'netmask',
582 'network_uuid': 'network_uuid',
583 'model': 'model',
584 'gateway': 'gateway',
585 'primary': 'primary',
586 'vrrp_vrid': 'vrrp_vrid',
587 'vrrp_primary_ip': 'vrrp_primary_ip',
588 'blocked-outgoing-ports': 'blocked_outgoing_ports'
589 },
590 'filesystem': {
591 'special': 'source',
592 'directory': 'target',
593 'type': 'type',
594 'raw': 'raw'
595 },
596 'disk': {
597 'boot': 'boot',
598 'image-size': 'image_size',
599 'image-name': 'image_name',
600 'image-uuid': 'image_uuid',
601 'match': 'path',
602 'media': 'media',
603 'model': 'model',
604 'size': 'size'
605 }
606 };
607
608 /*
609 * This allows one to define a function that will be run over the values from
610 * the zonecfg at the point where we transform that data into a VM object.
611 *
612 */
613 var XML_PROPERTY_TRANSFORMS = {
614 'alias': unbase64,
615 'archive_on_delete': fixBoolean,
616 'autoboot': fixBoolean,
617 'cpu_cap': numberify,
618 'cpu_shares': numberify,
619 'disks': {
620 'boot': fixBoolean,
621 'image_size': numberify,
622 'size': numberify
623 },
624 'do_not_inventory': fixBoolean,
625 'firewall_enabled': fixBoolean,
626 'max_locked_memory': unmangleMem,
627 'max_lwps': numberify,
628 'max_physical_memory': unmangleMem,
629 'max_swap': unmangleMem,
630 'never_booted': fixBoolean,
631 'nics': {
632 'dhcp_server': fixBoolean,
633 'allow_dhcp_spoofing': fixBoolean,
634 'allow_ip_spoofing': fixBoolean,
635 'allow_mac_spoofing': fixBoolean,
636 'allow_restricted_traffic': fixBoolean,
637 'allow_unfiltered_promisc': fixBoolean,
638 'allowed_ips': separateCommas,
639 'primary': fixBooleanLoose,
640 'vrrp_vrid': numberify,
641 'vlan_id': numberify
642 },
643 'qemu_extra_opts': unbase64,
644 'qemu_opts': unbase64,
645 'ram': numberify,
646 'restart_init': fixBoolean,
647 'resolvers': separateCommas,
648 'spice_password': unbase64,
649 'spice_port': numberify,
650 'spice_opts': unbase64,
651 'tmpfs': numberify,
652 'v': numberify,
653 'vcpus': numberify,
654 'virtio_txburst': numberify,
655 'virtio_txtimer': numberify,
656 'vnc_password': unbase64,
657 'vnc_port': numberify,
658 'zfs_io_priority': numberify,
659 'zoneid': numberify
660 };
661
662 /*
663 * This defines all of the possible properties that could be in a create/update
664 * payload and their types. Each of the entries are required to have at least
665 * a 'type' property which is one of:
666 *
667 * object-array -- an array of objects
668 * boolean -- true or false
669 * flat-object -- an object that has only string properties
670 * integer -- integers only
671 * list -- Either comma separated or array list of strings
672 * string -- Simple string
673 * uuid -- A standard 00000000-0000-0000-0000-000000000000 type uuid
674 * zpool -- The name of an existing zpool
675 *
676 */
677 var PAYLOAD_PROPERTIES = {
678 'add_disks': {'type': 'object-array', 'check_as': 'disks'},
679 'add_nics': {'type': 'object-array', 'check_as': 'nics'},
680 'alias': {'type': 'string'},
681 'archive_on_delete': {'type': 'boolean'},
682 'autoboot': {'type': 'boolean'},
683 'billing_id': {'type': 'string'},
684 'boot': {'type': 'string'},
685 'brand': {'type': 'string'},
686 'cpu_cap': {'type': 'integer'},
687 'cpu_shares': {'type': 'integer'},
688 'cpu_type': {'type': 'string'},
689 'create_only': {'type': 'boolean'},
690 'create_timestamp': {'type': 'string'},
691 'customer_metadata': {'type': 'flat-object'},
692 'dataset_uuid': {'type': 'uuid'},
693 'delegate_dataset': {'type': 'boolean'},
694 'disks': {'type': 'object-array'},
695 'disks.*.block_size': {'type': 'integer'},
696 'disks.*.boot': {'type': 'boolean'},
697 'disks.*.compression': {'type': 'string'},
698 'disks.*.image_name': {'type': 'string'},
699 'disks.*.image_size': {'type': 'integer'},
700 'disks.*.image_uuid': {'type': 'uuid'},
701 'disks.*.refreservation': {'type': 'integer'},
702 'disks.*.size': {'type': 'integer'},
703 'disks.*.media': {'type': 'string'},
704 'disks.*.model': {'type': 'string'},
705 'disks.*.nocreate': {'type': 'boolean'},
706 'disks.*.path': {'type': 'string'},
707 'disks.*.zpool': {'type': 'zpool'},
708 'disk_driver': {'type': 'string'},
709 'do_not_inventory': {'type': 'boolean'},
710 'dns_domain': {'type': 'string'},
711 'filesystems': {'type': 'object-array'},
712 'filesystems.*.type': {'type': 'string'},
713 'filesystems.*.source': {'type': 'string'},
714 'filesystems.*.target': {'type': 'string'},
715 'filesystems.*.raw': {'type': 'string'},
716 'filesystems.*.options': {'type': 'list'},
717 'firewall': {'type': 'object'},
718 'firewall_enabled': {'type': 'boolean'},
719 'fs_allowed': {'type': 'list'},
720 'hostname': {'type': 'string'},
721 'image_uuid': {'type': 'uuid'},
722 'init_name': {'type': 'string'},
723 'internal_metadata': {'type': 'flat-object'},
724 'limit_priv': {'type': 'list'},
725 'max_locked_memory': {'type': 'integer'},
726 'max_lwps': {'type': 'integer'},
727 'max_physical_memory': {'type': 'integer'},
728 'max_swap': {'type': 'integer'},
729 'mdata_exec_timeout': {'type': 'integer'},
730 'nics': {'type': 'object-array'},
731 'nics.*.allow_dhcp_spoofing': {'type': 'boolean'},
732 'nics.*.allow_ip_spoofing': {'type': 'boolean'},
733 'nics.*.allow_mac_spoofing': {'type': 'boolean'},
734 'nics.*.allow_restricted_traffic': {'type': 'boolean'},
735 'nics.*.allow_unfiltered_promisc': {'type': 'boolean'},
736 'nics.*.allowed_ips': {'type': 'list'},
737 'nics.*.blocked_outgoing_ports': {'type': 'list'},
738 'nics.*.dhcp_server': {'type': 'boolean'},
739 'nics.*.gateway': {'type': 'string'},
740 'nics.*.interface': {'type': 'string'},
741 'nics.*.ip': {'type': 'string'},
742 'nics.*.mac': {'type': 'string'},
743 'nics.*.model': {'type': 'string'},
744 'nics.*.netmask': {'type': 'string'},
745 'nics.*.network_uuid': {'type': 'uuid'},
746 'nics.*.nic_tag': {'type': 'string'},
747 'nics.*.primary': {'type': 'boolean'},
748 'nics.*.vrrp_vrid': {'type': 'integer-8bit'},
749 'nics.*.vrrp_primary_ip': {'type': 'string'},
750 'nics.*.vlan_id': {'type': 'integer'},
751 'nic_driver': {'type': 'string'},
752 'nowait': {'type': 'boolean'},
753 'owner_uuid': {'type': 'string'},
754 'package_name': {'type': 'string'},
755 'package_version': {'type': 'string'},
756 'qemu_opts': {'type': 'string'},
757 'qemu_extra_opts': {'type': 'string'},
758 'quota': {'type': 'integer'},
759 'ram': {'type': 'integer'},
760 'remove_customer_metadata': {'type': 'list'},
761 'remove_disks': {'type': 'list'},
762 'remove_internal_metadata': {'type': 'list'},
763 'remove_nics': {'type': 'list'},
764 'remove_routes': {'type': 'list'},
765 'remove_tags': {'type': 'list'},
766 'restart_init': {'type': 'boolean'},
767 'resolvers': {'type': 'list'},
768 'routes': {'type': 'flat-object'},
769 'set_routes': {'type': 'flat-object'},
770 'set_tags': {'type': 'flat-object'},
771 'set_customer_metadata': {'type': 'flat-object'},
772 'set_internal_metadata': {'type': 'flat-object'},
773 'spice_opts': {'type': 'string'},
774 'spice_password': {'type': 'string'},
775 'spice_port': {'type': 'integer'},
776 'tags': {'type': 'flat-object'},
777 'tmpfs': {'type': 'integer'},
778 'transition': {'type': 'flat-object'},
779 'update_disks': {'type': 'object-array', 'check_as': 'disks'},
780 'update_nics': {'type': 'object-array', 'check_as': 'nics'},
781 'uuid': {'type': 'uuid'},
782 'v': {'type': 'integer'},
783 'vcpus': {'type': 'integer'},
784 'vga': {'type': 'string'},
785 'virtio_txburst': {'type': 'integer'},
786 'virtio_txtimer': {'type': 'integer'},
787 'vnc_password': {'type': 'string'},
788 'vnc_port': {'type': 'integer'},
789 'zfs_data_compression': {'type': 'string'},
790 'zfs_data_recsize': {'type': 'integer'},
791 'zfs_io_priority': {'type': 'integer'},
792 'zfs_root_compression': {'type': 'string'},
793 'zfs_root_recsize': {'type': 'integer'},
794 'zone_dataset_uuid': {'type': 'uuid'},
795 'zonename': {'type': 'string'},
796 'zfs_storage_pool_name': {'type': 'zpool'},
797 'zpool': {'type': 'zpool'}
798 };
799
800 // shared between 'joyent' and 'joyent-minimal'
801 var joyent_allowed = {
802 'add_nics': ['update'],
803 'alias': ['create', 'receive', 'update'],
804 'archive_on_delete': ['create', 'receive', 'update'],
805 'autoboot': ['create', 'receive', 'update'],
806 'billing_id': ['create', 'receive', 'update'],
807 'brand': ['create', 'receive'],
808 'cpu_cap': ['create', 'receive', 'update'],
809 'cpu_shares': ['create', 'receive', 'update'],
810 'create_only': ['receive'],
811 'create_timestamp': ['receive'],
812 'customer_metadata': ['create', 'receive'],
813 'dataset_uuid': ['create', 'receive'],
814 'delegate_dataset': ['create', 'receive'],
815 'do_not_inventory': ['create', 'receive', 'update'],
816 'dns_domain': ['create', 'receive'],
817 'filesystems': ['create', 'receive'],
818 'filesystems.*.type': ['add'],
819 'filesystems.*.source': ['add'],
820 'filesystems.*.target': ['add'],
821 'filesystems.*.raw': ['add'],
822 'filesystems.*.options': ['add'],
823 'firewall': ['create'],
824 'firewall_enabled': ['create', 'receive', 'update'],
825 'fs_allowed': ['create', 'receive', 'update'],
826 'hostname': ['create', 'receive', 'update'],
827 'image_uuid': ['create', 'receive'],
828 'init_name': ['create', 'receive', 'update'],
829 'internal_metadata': ['create', 'receive'],
830 'limit_priv': ['create', 'receive', 'update'],
831 'max_locked_memory': ['create', 'receive', 'update'],
832 'max_lwps': ['create', 'receive', 'update'],
833 'max_physical_memory': ['create', 'receive', 'update'],
834 'max_swap': ['create', 'receive', 'update'],
835 'mdata_exec_timeout': ['create'],
836 'nics': ['create', 'receive'],
837 'nics.*.allow_dhcp_spoofing': ['add', 'update'],
838 'nics.*.allow_ip_spoofing': ['add', 'update'],
839 'nics.*.allow_mac_spoofing': ['add', 'update'],
840 'nics.*.allow_restricted_traffic': ['add', 'update'],
841 'nics.*.allowed_ips': ['add', 'update'],
842 'nics.*.blocked_outgoing_ports': ['add', 'update'],
843 'nics.*.dhcp_server': ['add', 'update'],
844 'nics.*.gateway': ['add', 'update'],
845 'nics.*.interface': ['add', 'update'],
846 'nics.*.ip': ['add', 'update'],
847 'nics.*.mac': ['add', 'update'],
848 'nics.*.netmask': ['add', 'update'],
849 'nics.*.network_uuid': ['add', 'update'],
850 'nics.*.nic_tag': ['add', 'update'],
851 'nics.*.vrrp_vrid': ['add', 'update'],
852 'nics.*.vrrp_primary_ip': ['add', 'update'],
853 'nics.*.primary': ['add', 'update'],
854 'nics.*.vlan_id': ['add', 'update'],
855 'nowait': ['create', 'receive'],
856 'owner_uuid': ['create', 'receive', 'update'],
857 'package_name': ['create', 'receive', 'update'],
858 'package_version': ['create', 'receive', 'update'],
859 'quota': ['create', 'receive', 'update'],
860 'ram': ['create', 'receive', 'update'],
861 'remove_customer_metadata': ['update'],
862 'remove_internal_metadata': ['update'],
863 'remove_nics': ['update'],
864 'remove_routes': ['update'],
865 'remove_tags': ['update'],
866 'restart_init': ['create', 'receive', 'update'],
867 'resolvers': ['create', 'receive', 'update'],
868 'routes': ['create', 'receive'],
869 'set_customer_metadata': ['update'],
870 'set_internal_metadata': ['update'],
871 'set_routes': ['update'],
872 'set_tags': ['update'],
873 'tags': ['create', 'receive'],
874 'tmpfs': ['create', 'receive', 'update'],
875 'transition': ['receive'],
876 'update_nics': ['update'],
877 'uuid': ['create', 'receive'],
878 'v': ['receive'],
879 'zfs_data_compression': ['create', 'receive', 'update'],
880 'zfs_data_recsize': ['create', 'receive', 'update'],
881 'zfs_io_priority': ['create', 'receive', 'update'],
882 'zfs_root_compression': ['create', 'receive', 'update'],
883 'zfs_root_recsize': ['create', 'receive', 'update'],
884 'zfs_storage_pool_name': ['create', 'receive'],
885 'zonename': ['create', 'receive'],
886 'zpool': ['create', 'receive']
887 };
888
889 /*
890 * This defines all of the properties allowed, required and features that a
891 * brand has. For each of the allowed/required properties you have a list of
892 * actions for which this is allowed/required. For properties that are lists
893 * of objects, you can specify the action as 'add' or 'update' for when you're
894 * adding or updating one of those objects.
895 *
896 * Features can currently be one of:
897 *
898 * 'cleanup_dataset' -- (boolean) whether to remove trash before booting
899 * 'default_memory_overhead' -- (integer) memory above 'ram' that's added
900 * 'limit_priv': (list) list of priviledges for this zone (if not 'default')
901 * 'mdata_restart' -- (boolean) whether the brand supports restarting its
902 * mdata:fetch service to update properties in the zone
903 * 'min_memory_overhead' -- (integer) minimum delta between ram + max_physical
904 * 'model_required' -- (boolean) whether a .model is required on nics and disks
905 * 'pid_file' -- (pathname) file containing the PID for zones with one process
906 * 'runtime_info' -- (boolean) whether this zone supports the 'info' command
907 * 'serial_console' -- (boolean) whether this zone uses serial console
908 * 'type' -- the type of the VM (OS or KVM), all brands should include this
909 * 'update_mdata_exec_timeout' (boolean) whether to update mdata:exec timeout
910 * 'update_rctls' (boolean) whether we can update rctls 'live' for this zone
911 * 'use_tmpfs' -- (boolean) whether this type of zone uses tmpfs
912 * 'use_vm_autoboot' -- (boolean) use vm-autoboot instead of autoboot
913 * 'use_vmadmd' -- (boolean) use vmadmd for some actions instead of direct
914 * 'var_svc_provisioning' -- (boolean) whether brand uses /var/svc/provisioning
915 * 'wait_for_hwsetup' -- (boolean) use QMP and provision_success when hwsetup
916 * 'write_zone_netfiles' -- (boolean) write out files like /etc/hostname.net0
917 * 'zlogin_console' -- (boolean) use zlogin -C for console (vs. serial_console)
918 * 'zoneinit' -- (boolean) this brand's setup may be controlled by zoneinit
919 *
920 * All of the keys:
921 *
922 * allowed_properties
923 * required_properties
924 * features
925 *
926 * should be defined for each brand. Even if empty.
927 */
928 var BRAND_OPTIONS = {
929 'joyent': {
930 'allowed_properties': joyent_allowed,
931 'required_properties': {
932 'brand': ['create', 'receive'],
933 'image_uuid': ['create', 'receive']
934 }, 'features': {
935 'brand_install_script': '/usr/lib/brand/joyent/jinstall',
936 'cleanup_dataset': true,
937 'mdata_restart': true,
938 'reprovision': true,
939 'type': 'OS',
940 'update_mdata_exec_timeout': true,
941 'update_rctls': true,
942 'use_tmpfs': true,
943 'write_zone_netfiles': true,
944 'zlogin_console': true,
945 'zoneinit': true
946 }
947 }, 'joyent-minimal': {
948 'allowed_properties': joyent_allowed,
949 'required_properties': {
950 'brand': ['create', 'receive'],
951 'image_uuid': ['create', 'receive']
952 }, 'features': {
953 'brand_install_script': '/usr/lib/brand/joyent-minimal/jinstall',
954 'cleanup_dataset': true,
955 'mdata_restart': true,
956 'reprovision': true,
957 'type': 'OS',
958 'update_mdata_exec_timeout': true,
959 'update_rctls': true,
960 'use_tmpfs': true,
961 'var_svc_provisioning': true,
962 'write_zone_netfiles': true,
963 'zlogin_console': true
964 }
965 }, 'sngl': {
966 'allowed_properties': joyent_allowed,
967 'required_properties': {
968 'brand': ['create', 'receive'],
969 'image_uuid': ['create', 'receive']
970 }, 'features': {
971 'cleanup_dataset': true,
972 'mdata_restart': true,
973 'type': 'OS',
974 'update_mdata_exec_timeout': true,
975 'update_rctls': true,
976 'use_tmpfs': true,
977 'write_zone_netfiles': true,
978 'zlogin_console': true,
979 'zoneinit': true
980 }
981 }, 'kvm': {
982 'allowed_properties': {
983 'add_disks': ['update'],
984 'add_nics': ['update'],
985 'alias': ['create', 'receive', 'update'],
986 'archive_on_delete': ['create', 'receive', 'update'],
987 'autoboot': ['create', 'receive', 'update'],
988 'billing_id': ['create', 'receive', 'update'],
989 'boot': ['create', 'receive', 'update'],
990 'brand': ['create', 'receive'],
991 'cpu_cap': ['create', 'receive', 'update'],
992 'cpu_shares': ['create', 'receive', 'update'],
993 'cpu_type': ['create', 'receive', 'update'],
994 'create_only': ['receive'],
995 'create_timestamp': ['receive'],
996 'customer_metadata': ['create', 'receive'],
997 'disks': ['create', 'receive'],
998 'disks.*.block_size': ['add'],
999 'disks.*.boot': ['add', 'update'],
1000 'disks.*.compression': ['add', 'update'],
1001 'disks.*.image_name': ['add', 'update'],
1002 'disks.*.image_size': ['add'],
1003 'disks.*.image_uuid': ['add'],
1004 'disks.*.refreservation': ['add', 'update'],
1005 'disks.*.size': ['add'],
1006 'disks.*.media': ['add', 'update'],
1007 'disks.*.model': ['add', 'update'],
1008 'disks.*.nocreate': ['add'],
1009 'disks.*.path': ['add', 'update'],
1010 'disks.*.zpool': ['add'],
1011 'disk_driver': ['create', 'receive', 'update'],
1012 'do_not_inventory': ['create', 'receive', 'update'],
1013 'firewall': ['create'],
1014 'firewall_enabled': ['create', 'receive', 'update'],
1015 'hostname': ['create', 'receive', 'update'],
1016 'image_uuid': ['create', 'receive'],
1017 'internal_metadata': ['create', 'receive'],
1018 'limit_priv': ['create', 'receive', 'update'],
1019 'max_locked_memory': ['create', 'receive', 'update'],
1020 'max_lwps': ['create', 'receive', 'update'],
1021 'max_physical_memory': ['create', 'receive', 'update'],
1022 'max_swap': ['create', 'receive', 'update'],
1023 'nics': ['create', 'receive'],
1024 'nics.*.allow_dhcp_spoofing': ['add', 'update'],
1025 'nics.*.allow_ip_spoofing': ['add', 'update'],
1026 'nics.*.allow_mac_spoofing': ['add', 'update'],
1027 'nics.*.allow_restricted_traffic': ['add', 'update'],
1028 'nics.*.allow_unfiltered_promisc': ['add', 'update'],
1029 'nics.*.allowed_ips': ['add', 'update'],
1030 'nics.*.blocked_outgoing_ports': ['add', 'update'],
1031 'nics.*.dhcp_server': ['add', 'update'],
1032 'nics.*.gateway': ['add', 'update'],
1033 'nics.*.interface': ['add', 'update'],
1034 'nics.*.ip': ['add', 'update'],
1035 'nics.*.mac': ['add', 'update'],
1036 'nics.*.model': ['add', 'update'],
1037 'nics.*.netmask': ['add', 'update'],
1038 'nics.*.network_uuid': ['add', 'update'],
1039 'nics.*.nic_tag': ['add', 'update'],
1040 'nics.*.primary': ['add', 'update'],
1041 'nics.*.vlan_id': ['add', 'update'],
1042 'nic_driver': ['create', 'receive', 'update'],
1043 'owner_uuid': ['create', 'receive', 'update'],
1044 'package_name': ['create', 'receive', 'update'],
1045 'package_version': ['create', 'receive', 'update'],
1046 'qemu_opts': ['create', 'receive', 'update'],
1047 'qemu_extra_opts': ['create', 'receive', 'update'],
1048 'quota': ['create', 'receive', 'update'],
1049 'ram': ['create', 'receive', 'update'],
1050 'remove_customer_metadata': ['update'],
1051 'remove_disks': ['update'],
1052 'remove_internal_metadata': ['update'],
1053 'remove_nics': ['update'],
1054 'remove_routes': ['update'],
1055 'remove_tags': ['update'],
1056 'resolvers': ['create', 'receive', 'update'],
1057 'set_customer_metadata': ['update'],
1058 'set_internal_metadata': ['update'],
1059 'set_routes': ['update'],
1060 'set_tags': ['update'],
1061 'spice_opts': ['create', 'receive', 'update'],
1062 'spice_password': ['create', 'receive', 'update'],
1063 'spice_port': ['create', 'receive', 'update'],
1064 'tags': ['create', 'receive'],
1065 'transition': ['receive'],
1066 'update_disks': ['update'],
1067 'update_nics': ['update'],
1068 'uuid': ['create', 'receive'],
1069 'v': ['receive'],
1070 'vcpus': ['create', 'receive', 'update'],
1071 'vga': ['create', 'receive', 'update'],
1072 'virtio_txburst': ['create', 'receive', 'update'],
1073 'virtio_txtimer': ['create', 'receive', 'update'],
1074 'vnc_password': ['create', 'receive', 'update'],
1075 'vnc_port': ['create', 'receive', 'update'],
1076 'zfs_io_priority': ['create', 'receive', 'update'],
1077 'zfs_root_compression': ['create', 'receive', 'update'],
1078 'zfs_root_recsize': ['create', 'receive', 'update'],
1079 'zone_dataset_uuid': ['create', 'receive'],
1080 'zpool': ['create', 'receive']
1081 }, 'required_properties': {
1082 'brand': ['create', 'receive']
1083 }, 'features': {
1084 'default_memory_overhead': VM.KVM_MEM_OVERHEAD,
1085 'limit_priv': ['default', '-file_link_any', '-net_access',
1086 '-proc_fork', '-proc_info', '-proc_session'],
1087 'min_memory_overhead': VM.KVM_MIN_MEM_OVERHEAD,
1088 'model_required': true,
1089 'pid_file': '/tmp/vm.pid',
1090 'runtime_info': true,
1091 'serial_console': true,
1092 'type': 'KVM',
1093 'use_vm_autoboot': true,
1094 'use_vmadmd': true,
1095 'var_svc_provisioning': true,
1096 'wait_for_hwsetup': true
1097 }
1098 }
1099 };
1100
1101 var VIRTIO_TXTIMER_DEFAULT = 200000;
1102 var VIRTIO_TXBURST_DEFAULT = 128;
1103
1104 function getZpools(log, callback)
1105 {
1106 var args = ['list', '-H', '-p', '-o', 'name'];
1107 var cmd = '/usr/sbin/zpool';
1108 var idx;
1109 var raw = [];
1110 var zpools = [];
1111
1112 assert(log, 'no logger passed to getZpools()');
1113
1114 log.debug(cmd + ' ' + args.join(' '));
1115 execFile(cmd, args, function (error, stdout, stderr) {
1116 if (error) {
1117 log.error('Unable to get list of zpools');
1118 callback(error, {'stdout': stdout, 'stderr': stderr});
1119 } else {
1120 // strip out any empty values (last one).
1121 raw = stdout.split('\n');
1122 for (idx in raw) {
1123 if (raw[idx].length > 0) {
1124 zpools.push(raw[idx]);
1125 }
1126 }
1127 callback(null, zpools);
1128 }
1129 });
1130 }
1131
1132 /*
1133 * When you need to access files inside a zoneroot, you need to be careful that
1134 * there are no symlinks in the path. Since we operate from the GZ, these
1135 * symlinks will be evaluated in the GZ context. Eg. a symlink in zone A with
1136 * /var/run -> * /zones/<uuid of zone B>/root/var/run would mean that operating
1137 * on files in zone A's /var/run would actually be touching files in zone B.
1138 *
1139 * To prevent that, only ever modify files inside the zoneroot from the GZ
1140 * *before* first boot. After the zone is booted, it's better to use services
1141 * in the zone to pull values from metadata and write out changes on next boot.
1142 * It's also safe to use zlogin when the zone is running.
1143 *
1144 * This function is intended to be used in those cases we do write things out
1145 * before the zone's first boot but the dataset might have invalid symlinks in
1146 * it even then, so we still need to confirm the paths inside zoneroot before
1147 * using them. It throws an exception if:
1148 *
1149 * - zoneroot is not an absolute path
1150 * - fs.lstatSync fails
1151 * - target path under zoneroot contains symlink
1152 * - a component leading up to the final one is not a directory
1153 * - options.type is set to 'file' and target is not a regular file
1154 * - options.type is set to 'dir' and target references a non-directory
1155 * - options.type is not one of 'file' or 'dir'
1156 * - options.enoent_ok is false and target path doesn't exist
1157 *
1158 * if none of those are the case, it returns true.
1159 */
1160 function assertSafeZonePath(zoneroot, target, options)
1161 {
1162 var parts;
1163 var root;
1164 var stat;
1165 var test;
1166
1167 assert((zoneroot.length > 0 && zoneroot[0] === '/'),
1168 'zoneroot must be an absolute path not: [' + zoneroot + ']');
1169
1170 parts = trim(target, '/').split('/');
1171 root = trim(zoneroot, '/');
1172 test = '/' + root;
1173
1174 while (parts.length > 0) {
1175 test = test + '/' + parts.shift();
1176
1177 try {
1178 stat = fs.lstatSync(test);
1179 } catch (e) {
1180 if (e.code === 'ENOENT') {
1181 if (!options.hasOwnProperty('enoent_ok')
1182 || options.enoent_ok === false) {
1183
1184 throw e;
1185 } else {
1186 // enoent is ok, return true. This is mostly used when
1187 // deleting files with rm -f <path>. It's ok for <path> to
1188 // not exist (but not ok for any component to be a symlink)
1189 // there's no point continuing though since ENOENT here
1190 // means all subpaths also won't exist.
1191 return true;
1192 }
1193 } else {
1194 throw e;
1195 }
1196 }
1197
1198 if (stat.isSymbolicLink()) {
1199 // it's never ok to have a symlink component
1200 throw new Error(test + ' is a symlink');
1201 }
1202
1203 // any component other than the last also needs to be a
1204 // directory, last can also be a file.
1205 if (parts.length === 0) {
1206 // last, dir or file
1207 if (!options.hasOwnProperty('type') || options.type === 'dir') {
1208 if (!stat.isDirectory()) {
1209 throw new Error(test + ' is not a directory');
1210 }
1211 } else if (options.type === 'file') {
1212 if (!stat.isFile()) {
1213 throw new Error(test + ' is not a file');
1214 }
1215 } else {
1216 throw new Error('this function does not know about '
1217 + options.type);
1218 }
1219 } else if (!stat.isDirectory()) {
1220 // not last component, only dir is acceptable
1221 throw new Error(test + ' is not a directory');
1222 }
1223 }
1224 // if we didn't throw, this is valid.
1225 return true;
1226 }
1227
1228 function validateProperty(brand, prop, value, action, data, errors, log)
1229 {
1230 var allowed;
1231 var k;
1232
1233 assert(log, 'no logger passed to validateProperty()');
1234
1235 if (!data.hasOwnProperty('zpools')) {
1236 data.zpools = [];
1237 }
1238
1239 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')) {
1240 allowed = BRAND_OPTIONS[brand].allowed_properties;
1241 } else {
1242 allowed = {};
1243 }
1244
1245 if (!errors.hasOwnProperty('bad_values')) {
1246 errors.bad_values = [];
1247 }
1248 if (!errors.hasOwnProperty('bad_properties')) {
1249 errors.bad_properties = [];
1250 }
1251
1252 if (!allowed.hasOwnProperty(prop)) {
1253 // thie BRAND_OPTIONS doesn't have this property at all
1254 if (errors.bad_properties.indexOf(prop) === -1) {
1255 errors.bad_properties.push(prop);
1256 }
1257 } else if (!Array.isArray(allowed[prop])
1258 || allowed[prop].indexOf(action) === -1) {
1259
1260 // here we've ether got no actions allowed for this value,
1261 // or just not this one
1262 if (errors.bad_properties.indexOf(prop) === -1) {
1263 errors.bad_properties.push(prop);
1264 }
1265 }
1266
1267 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)) {
1268 switch (PAYLOAD_PROPERTIES[prop].type) {
1269 case 'uuid':
1270 if (typeof (value) === 'string' && !isUUID(value)
1271 && errors.bad_values.indexOf(prop) === -1) {
1272
1273 errors.bad_values.push(prop);
1274 }
1275 break;
1276 case 'boolean':
1277 if (value === 1 || value === '1') {
1278 log.warn('DEPRECATED: payload uses 1 instead of '
1279 + 'true for ' + prop + ', use "true" instead.');
1280 } else if (typeof (fixBoolean(value)) !== 'boolean'
1281 && errors.bad_values.indexOf(prop) === -1) {
1282
1283 errors.bad_values.push(prop);
1284 }
1285 break;
1286 case 'string':
1287 if (value === undefined || value === null
1288 || trim(value.toString()) === '') {
1289 // if set empty/false we'll keep since this is used to unset
1290 break;
1291 } else if (typeof (value) !== 'string'
1292 && errors.bad_values.indexOf(prop) === -1) {
1293
1294 errors.bad_values.push(prop);
1295 }
1296 break;
1297 case 'integer':
1298 if (value === undefined || value === null
1299 || trim(value.toString()) === '') {
1300 // if set empty/false we'll keep since this is used to unset
1301 break;
1302 } else if (((typeof (value) !== 'string'
1303 && typeof (value) !== 'number')
1304 || !value.toString().match(/^[0-9]+$/))
1305 && errors.bad_values.indexOf(prop) === -1) {
1306
1307 if ((['vnc_port', 'spice_port'].indexOf(prop) !== -1)
1308 && (value.toString() === '-1')) {
1309
1310 // these keys allow '-1' as a value, so we succeed here even
1311 // though we'd otherwise fail.
1312 break;
1313 }
1314
1315 errors.bad_values.push(prop);
1316 } else if (prop === 'max_swap' && value < MINIMUM_MAX_SWAP) {
1317 errors.bad_values.push(prop);
1318 }
1319 break;
1320 case 'integer-8bit':
1321 if (value === undefined || value === null
1322 || trim(value.toString()) === '') {
1323 // if set empty/false we'll keep since this is used to unset
1324 break;
1325 } else if (((typeof (value) !== 'string'
1326 && typeof (value) !== 'number')
1327 || !value.toString().match(/^[0-9]+$/))
1328 && errors.bad_values.indexOf(prop) === -1
1329 ) {
1330
1331 errors.bad_values.push(prop);
1332 break;
1333 }
1334 if (value < 0 || value > 255) {
1335 errors.bad_values.push(prop);
1336 }
1337 break;
1338 case 'zpool':
1339 if ((typeof (value) !== 'string'
1340 || data.zpools.indexOf(value) === -1)
1341 && errors.bad_values.indexOf(prop) === -1) {
1342
1343 errors.bad_values.push(prop);
1344 }
1345 break;
1346 case 'object':
1347 if (typeof (value) !== 'object'
1348 && errors.bad_values.indexOf(prop) === -1) {
1349
1350 errors.bad_values.push(prop);
1351 }
1352 break;
1353 case 'flat-object':
1354 if (typeof (value) !== 'object'
1355 && errors.bad_values.indexOf(prop) === -1) {
1356
1357 errors.bad_values.push(prop);
1358 }
1359 for (k in value) {
1360 if (typeof (value[k]) !== 'string'
1361 && typeof (value[k]) !== 'number'
1362 && typeof (value[k]) !== 'boolean') {
1363
1364 if (errors.bad_values.indexOf(prop) === -1) {
1365 errors.bad_values.push(prop);
1366 }
1367 break;
1368 }
1369 }
1370 break;
1371 case 'list':
1372 if (typeof (value) === 'string') {
1373 // really any string could be valid (a one element list)
1374 break;
1375 } else if (Array.isArray(value)) {
1376 for (k in value) {
1377 if (typeof (value[k]) !== 'string'
1378 && typeof (value[k]) !== 'number') {
1379
1380 // TODO: log something more useful here telling them
1381 // the type is invalid.
1382 if (errors.bad_values.indexOf(prop) === -1) {
1383 errors.bad_values.push(prop);
1384 }
1385 break;
1386 }
1387 // if this is an array, it can't have commas in the
1388 // values. (since we might stringify the list and
1389 // we'd end up with something different.
1390 if (value[k].toString().indexOf(',') !== -1
1391 && errors.bad_values.indexOf(prop) === -1) {
1392
1393 errors.bad_values.push(prop);
1394 }
1395 }
1396 } else {
1397 // not a valid type
1398 if (errors.bad_values.indexOf(prop) === -1) {
1399 errors.bad_values.push(prop);
1400 }
1401 }
1402 break;
1403 case 'object-array':
1404 if (!Array.isArray(value)) {
1405 if (errors.bad_values.indexOf(prop) === -1) {
1406 errors.bad_values.push(prop);
1407 }
1408 break;
1409 }
1410 for (k in value) {
1411 if (typeof (value[k]) !== 'object') {
1412 if (errors.bad_values.indexOf(prop) === -1) {
1413 errors.bad_values.push(prop);
1414 }
1415 break;
1416 }
1417 }
1418 break;
1419 default:
1420 // don't know what type of prop this is, so it's invalid
1421 if (errors.bad_properties.indexOf(prop) === -1) {
1422 errors.bad_properties.push(prop);
1423 }
1424 break;
1425 }
1426 }
1427 }
1428
1429 /*
1430 * image properties:
1431 *
1432 * size (optional, only used by zvols)
1433 * type ('zvol' or 'zone-dataset')
1434 * uuid
1435 * zpool
1436 *
1437 */
1438 function validateImage(image, log, callback)
1439 {
1440 var args;
1441 var cmd = '/usr/sbin/imgadm';
1442
1443 args = ['get', '-P', image.zpool, image.uuid];
1444
1445 log.debug(cmd + ' ' + args.join(' '));
1446
1447 // on any error we fail closed (assume the image does not exist)
1448 execFile(cmd, args, function (error, stdout, stderr) {
1449 var data;
1450 var e;
1451
1452 if (error) {
1453 error.stdout = stdout;
1454 error.stderr = stderr;
1455 error.whatFailed = 'EEXECFILE';
1456 log.error(error);
1457 callback(error);
1458 return;
1459 }
1460
1461 try {
1462 data = JSON.parse(stdout.toString());
1463 } catch (err) {
1464 data = {};
1465 }
1466
1467 if (data.hasOwnProperty('manifest')) {
1468 if (data.manifest.type !== image.type) {
1469 // image is wrong type
1470 e = new Error('image ' + image.uuid + ' is type '
1471 + data.manifest.type + ', must be ' + image.type);
1472 e.whatFailed = 'EBADTYPE';
1473 log.error(e);
1474 callback(e);
1475 return;
1476 }
1477 log.info('image ' + image.uuid + ' found in imgadm');
1478
1479 // If image_size is missing, add it. If it's wrong, error.
1480 if (data.manifest.hasOwnProperty('image_size')) {
1481 if (image.hasOwnProperty('size')) {
1482 if (image.size !== data.manifest.image_size) {
1483 e = new Error('incorrect image_size value for image'
1484 + ' ' + image.uuid + ' passed: '
1485 + image.size + ' should be: '
1486 + data.manifest.image_size);
1487 e.whatFailed = 'EBADSIZE';
1488 log.error(e);
1489 callback(e);
1490 return;
1491 }
1492 } else {
1493 // image doesn't have size, manifest does, add it.
1494 image.size = data.manifest.image_size;
1495 }
1496 }
1497 // everything ok
1498 callback();
1499 } else {
1500 e = new Error('cannot find \'manifest\' for image '
1501 + image.uuid);
1502 e.whatFailed = 'ENOENT';
1503 log.error(e);
1504 callback(e);
1505 return;
1506 }
1507 });
1508 }
1509
1510 // Ensure if image_uuid is passed either at top level or for disks.*.image_uuid
1511 // that image_uuid exists on the system according to imgadm.
1512 //
1513 // NOTE: if image_size is missing from payload, but found in imgadm it is added
1514 // to the payload here.
1515 //
1516 function validateImages(payload, errors, log, callback)
1517 {
1518 var check_images = [];
1519 var disk_idx;
1520 var pool;
1521
1522 if (payload.hasOwnProperty('image_uuid') && isUUID(payload.image_uuid)) {
1523 if (payload.hasOwnProperty('zpool')) {
1524 pool = payload.zpool;
1525 } else {
1526 pool = 'zones';
1527 }
1528
1529 check_images.push({
1530 'property': 'image_uuid',
1531 'target': payload,
1532 'type': 'zone-dataset',
1533 'uuid': payload.image_uuid,
1534 'zpool': pool
1535 });
1536 }
1537
1538 ['disks', 'add_disks'].forEach(function (d) {
1539 if (payload.hasOwnProperty(d)) {
1540 disk_idx = 0;
1541 payload[d].forEach(function (disk) {
1542 if (disk.hasOwnProperty('image_uuid')) {
1543 if (disk.hasOwnProperty('zpool')) {
1544 pool = disk.zpool;
1545 } else {
1546 pool = 'zones';
1547 }
1548 check_images.push({
1549 'property_prefix': d + '.' + disk_idx,
1550 'property': d + '.' + disk_idx + '.image_uuid',
1551 'target': disk,
1552 'type': 'zvol',
1553 'uuid': disk.image_uuid,
1554 'zpool': pool
1555 });
1556 }
1557 disk_idx++;
1558 });
1559 }
1560 });
1561
1562 async.forEachSeries(check_images, function (image, cb) {
1563
1564 var i;
1565 var idx;
1566
1567 i = {
1568 uuid: image.uuid,
1569 type: image.type,
1570 zpool: image.zpool
1571 };
1572
1573 if (image.target.hasOwnProperty('image_size')) {
1574 i.size = image.target.image_size;
1575 }
1576
1577 validateImage(i, log, function (err) {
1578 if (err) {
1579 switch (err.whatFailed) {
1580 case 'EBADSIZE':
1581 // image.size is wrong (vs. manifest)
1582 errors.bad_values.push(image.property_prefix
1583 + '.image_size');
1584 break;
1585 case 'ENOENT':
1586 // image.uuid not found in imgadm
1587 errors.bad_values.push(image.property);
1588 break;
1589 case 'EBADTYPE':
1590 // image.type is wrong
1591 errors.bad_values.push(image.property);
1592 break;
1593 default:
1594 // unknown error, fail closed
1595 errors.bad_values.push(image.property);
1596 break;
1597 }
1598 } else {
1599 // no errors, so check if size was added
1600 if (i.hasOwnProperty('size')) {
1601 if (!image.target.hasOwnProperty('image_size')) {
1602 image.target.image_size = i.size;
1603 // Remove error that would have been added earlier
1604 // when we didn't have image_size
1605 idx = errors.missing_properties.indexOf(
1606 image.property_prefix + '.image_size');
1607 if (idx !== -1) {
1608 errors.missing_properties.splice(idx, 1);
1609 }
1610 }
1611 }
1612 }
1613
1614 cb();
1615 });
1616 }, function () {
1617 callback();
1618 });
1619 }
1620
1621 // This is for allowed_ips which accepts IPiv4 addresses or CIDR addresses in
1622 // the form IP/MASK where MASK is 1-32.
1623 function validateIPlist(list) {
1624 var invalid = [];
1625
1626 list.forEach(function (ip) {
1627 var matches;
1628 if (!net.isIPv4(ip)) {
1629 matches = ip.match(/^([0-9\.]+)\/([0-9]+)$/);
1630 if (matches && net.isIPv4(matches[1])
1631 && (Number(matches[2]) >= 1) && (Number(matches[2]) <= 32)) {
1632
1633 // In this case it wasn't an IPv4, but it was a valid CIDR
1634 return;
1635 } else {
1636 invalid.push(ip);
1637 }
1638 }
1639 });
1640
1641 if (invalid.length !== 0) {
1642 throw new Error('invalid allowed_ips: ' + invalid.join(', '));
1643 }
1644
1645 if (list.length > 13) {
1646 throw new Error('Maximum of 13 allowed_ips per nic');
1647 }
1648 }
1649
1650 exports.validate = function (brand, action, payload, options, callback)
1651 {
1652 var errors = {
1653 'bad_values': [],
1654 'bad_properties': [],
1655 'missing_properties': []
1656 };
1657 var log;
1658 var prop;
1659
1660 // options is optional
1661 if (arguments.length === 4) {
1662 callback = arguments[3];
1663 options = {};
1664 }
1665
1666 ensureLogging(false);
1667 if (options.hasOwnProperty('log')) {
1668 log = options.log;
1669 } else {
1670 log = VM.log.child({action: 'validate'});
1671 }
1672
1673 if (!BRAND_OPTIONS.hasOwnProperty(brand)) {
1674 if (!brand) {
1675 brand = 'undefined';
1676 }
1677 callback({'bad_brand': brand});
1678 return;
1679 }
1680
1681 // wrap the whole thing with getZpools so we have the list of pools if we
1682 // need them.
1683 getZpools(log, function (err, zpools) {
1684 var disk_idx;
1685 var idx;
1686 var prefix;
1687 var required;
1688 var subprop;
1689 var subprop_action = '';
1690 var value;
1691
1692 if (err) {
1693 /*
1694 * this only happens when the zpool command fails which should be
1695 * very rare, but when it does happen, we continue with an empty
1696 * zpool list in case they don't need to validate zpools. If they
1697 * do, every zpool will be invalid which is also what we want since
1698 * nothing else that uses zpools is likely to work either.
1699 *
1700 */
1701 zpools = [];
1702 }
1703
1704 // loop through and weed out ones we don't allow for this action.
1705 for (prop in payload) {
1706 validateProperty(brand, prop, payload[prop], action,
1707 {zpools: zpools}, errors, log);
1708
1709 // special case for complex properties where we want to check
1710 // foo.*.whatever
1711 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)
1712 && PAYLOAD_PROPERTIES[prop].type === 'object-array'
1713 && Array.isArray(payload[prop])) {
1714
1715 if (PAYLOAD_PROPERTIES[prop].hasOwnProperty('check_as')) {
1716 prefix = PAYLOAD_PROPERTIES[prop].check_as + '.*.';
1717 if (prop.match(/^add_/)) {
1718 subprop_action = 'add';
1719 } else if (prop.match(/^update_/)) {
1720 subprop_action = 'update';
1721 }
1722 } else {
1723 // here we've got something like 'disks' which is an add
1724 prefix = prop + '.*.';
1725 subprop_action = 'add';
1726 }
1727
1728 for (idx in payload[prop]) {
1729 if (typeof (payload[prop][idx]) === 'object') {
1730 // subprop will be something like 'nic_tag'
1731 for (subprop in payload[prop][idx]) {
1732 value = payload[prop][idx][subprop];
1733 validateProperty(brand, prefix + subprop, value,
1734 subprop_action, {zpools: zpools}, errors, log);
1735 }
1736 } else if (errors.bad_values.indexOf(prop) === -1) {
1737 // this is not an object so bad value in the array
1738 errors.bad_values.push(prop);
1739 }
1740 }
1741 }
1742 }
1743
1744 // special case: if you have disks you must specify either image_uuid
1745 // and image_size *or* size and block_size is only allowed when you use
1746 // 'size' and image_name when you don't.
1747 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')
1748 && BRAND_OPTIONS[brand].allowed_properties
1749 .hasOwnProperty('disks')) {
1750
1751 function validateDiskSource(prop_prefix, disk) {
1752
1753 if (disk.hasOwnProperty('media') && disk.media !== 'disk') {
1754 // we only care about disks here, not cdroms.
1755 return;
1756 }
1757
1758 if (disk.hasOwnProperty('image_uuid')) {
1759 // with image_uuid, size is invalid and image_size is
1760 // required, additionally block_size is not allowed.
1761
1762 if (!disk.hasOwnProperty('image_size')) {
1763 errors.missing_properties.push(prop_prefix
1764 + '.image_size');
1765 }
1766 if (disk.hasOwnProperty('size')) {
1767 errors.bad_properties.push(prop_prefix + '.size');
1768 }
1769 if (disk.hasOwnProperty('block_size')) {
1770 errors.bad_properties.push(prop_prefix
1771 + '.block_size');
1772 }
1773 } else {
1774 // without image_uuid, image_size and image_name are invalid
1775 // and 'size' is required.
1776
1777 if (!disk.hasOwnProperty('size')) {
1778 errors.missing_properties.push(prop_prefix + '.size');
1779 }
1780 if (disk.hasOwnProperty('image_name')) {
1781 errors.bad_properties.push(prop_prefix + '.image_name');
1782 }
1783 if (disk.hasOwnProperty('image_size')) {
1784 errors.bad_properties.push(prop_prefix + '.image_size');
1785 }
1786 }
1787 }
1788
1789 if (payload.hasOwnProperty('disks')) {
1790 for (disk_idx in payload.disks) {
1791 validateDiskSource('disks.' + disk_idx,
1792 payload.disks[disk_idx]);
1793 }
1794 }
1795 if (payload.hasOwnProperty('add_disks')) {
1796 for (disk_idx in payload.add_disks) {
1797 validateDiskSource('add_disks.' + disk_idx,
1798 payload.add_disks[disk_idx]);
1799 }
1800 }
1801 }
1802
1803 if (BRAND_OPTIONS[brand].hasOwnProperty('required_properties')) {
1804 required = BRAND_OPTIONS[brand].required_properties;
1805 for (prop in required) {
1806 if (required[prop].indexOf(action) !== -1
1807 && !payload.hasOwnProperty(prop)) {
1808
1809 errors.missing_properties.push(prop);
1810 }
1811 }
1812 }
1813
1814 // make sure any images in the payload are also valid
1815 // NOTE: if validateImages() finds errors, it adds to 'errors' here.
1816 validateImages(payload, errors, log, function () {
1817
1818 // we validate disks.*.refreservation here because image_size might
1819 // not be populated yet until we return from validateImages()
1820 ['disks', 'add_disks'].forEach(function (d) {
1821 var d_idx = 0;
1822 if (payload.hasOwnProperty(d)) {
1823 payload[d].forEach(function (disk) {
1824 if (disk.hasOwnProperty('refreservation')) {
1825 if (disk.refreservation < 0) {
1826 errors.bad_values.push(d + '.' + d_idx
1827 + '.refreservation');
1828 } else if (disk.size
1829 && disk.refreservation > disk.size) {
1830
1831 errors.bad_values.push(d + '.' + d_idx
1832 + '.refreservation');
1833 } else if (disk.image_size
1834 && disk.refreservation > disk.image_size) {
1835
1836 errors.bad_values.push(d + '.' + d_idx
1837 + '.refreservation');
1838 }
1839 }
1840 d_idx++;
1841 });
1842 }
1843 });
1844
1845 if (errors.bad_properties.length > 0 || errors.bad_values.length > 0
1846 || errors.missing_properties.length > 0) {
1847
1848 callback(errors);
1849 return;
1850 }
1851
1852 callback();
1853 });
1854 });
1855 };
1856
1857 function separateCommas(str)
1858 {
1859 return str.split(',');
1860 }
1861
1862 function unmangleMem(str)
1863 {
1864 return (Number(str) / (1024 * 1024));
1865 }
1866
1867 function unbase64(str)
1868 {
1869 return new Buffer(str, 'base64').toString('ascii');
1870 }
1871
1872 function numberify(str)
1873 {
1874 return Number(str);
1875 }
1876
1877 function startElement(name, attrs, state, log) {
1878 var disk = {};
1879 var key;
1880 var newobj;
1881 var nic = {};
1882 var obj;
1883 var prop;
1884 var stack;
1885 var use;
1886 var where;
1887
1888 assert(log, 'no logger passed to startElement()');
1889
1890 if (!state.hasOwnProperty('stack')) {
1891 state.stack = [];
1892 }
1893 obj = state.obj;
1894 stack = state.stack;
1895
1896 stack.push(name);
1897 where = stack.join('.');
1898
1899 if (XML_PROPERTIES.hasOwnProperty(where)) {
1900 for (key in XML_PROPERTIES[where]) {
1901 use = XML_PROPERTIES[where][key];
1902 if (attrs.hasOwnProperty(key)) {
1903 obj[use] = attrs[key];
1904 } else if (attrs.hasOwnProperty('name') && attrs.name === key) {
1905 // attrs use the whacky {name, type, value} stuff.
1906 obj[use] = attrs['value'];
1907 }
1908 }
1909 } else if (where === 'zone.rctl') {
1910 stack.push(attrs.name);
1911 } else if (where === 'zone.network') {
1912 // new network device
1913 for (prop in attrs) {
1914 if (XML_PROPERTIES.nic.hasOwnProperty(prop)) {
1915 use = XML_PROPERTIES.nic[prop];
1916 if (prop === 'mac-addr') {
1917 // XXX SmartOS inherited the ridiculous MAC formatting from
1918 // Solaris where leading zeros are removed. We should
1919 // Fix that in the OS tools.
1920 nic[use] = fixMac(attrs[prop]);
1921 } else {
1922 nic[use] = attrs[prop];
1923 }
1924 } else {
1925 log.debug('unknown net prop: ' + prop);
1926 }
1927 }
1928 if (!obj.hasOwnProperty('networks')) {
1929 obj.networks = {};
1930 }
1931 obj.networks[nic.mac] = nic;
1932 stack.push(nic.mac);
1933 } else if (where.match(/zone\.network\...:..:..:..:..:..\.net-attr/)) {
1934 if (XML_PROPERTIES.nic.hasOwnProperty(attrs.name)) {
1935 use = XML_PROPERTIES.nic[attrs.name];
1936 obj.networks[stack[2]][use] = attrs.value;
1937 } else {
1938 log.debug('unknown net prop: ' + attrs.name);
1939 }
1940 } else if (where === 'zone.device') {
1941 // new disk device
1942 for (prop in attrs) {
1943 if (XML_PROPERTIES.disk.hasOwnProperty(prop)) {
1944 use = XML_PROPERTIES.disk[prop];
1945 disk[use] = attrs[prop];
1946 } else {
1947 log.debug('unknown disk prop: ' + prop);
1948 }
1949 }
1950 if (!obj.hasOwnProperty('devices')) {
1951 obj.devices = {};
1952 }
1953 obj.devices[disk.path] = disk;
1954 stack.push(disk.path);
1955 } else if (where.match(/zone\.device\.\/.*\.net-attr/)) {
1956 if (XML_PROPERTIES.disk.hasOwnProperty(attrs.name)) {
1957 use = XML_PROPERTIES.disk[attrs.name];
1958 obj.devices[stack[2]][use] = attrs.value;
1959 } else {
1960 log.debug('unknown disk prop: ' + attrs.name);
1961 }
1962 } else if (where === 'zone.dataset') {
1963 if (!obj.hasOwnProperty('datasets')) {
1964 obj.datasets = [];
1965 }
1966 if (attrs.hasOwnProperty('name')) {
1967 obj.datasets.push(attrs.name);
1968 }
1969 } else if (where === 'zone.filesystem') {
1970 if (!obj.hasOwnProperty('filesystems')) {
1971 obj.filesystems = [];
1972 }
1973 newobj = {};
1974 for (prop in XML_PROPERTIES.filesystem) {
1975 if (attrs.hasOwnProperty(prop)) {
1976 newobj[XML_PROPERTIES.filesystem[prop]] = attrs[prop];
1977 }
1978 }
1979 obj.filesystems.push(newobj);
1980 } else if (where === 'zone.filesystem.fsoption') {
1981 newobj = obj.filesystems.slice(-1)[0]; // the last element
1982 if (!newobj.hasOwnProperty('options')) {
1983 newobj.options = [];
1984 }
1985 newobj.options.push(attrs.name);
1986 } else {
1987 log.debug('unknown property: ' + where + ': '
1988 + JSON.stringify(attrs));
1989 }
1990 }
1991
1992 function endElement(name, state) {
1993 // trim stack back above this element
1994 var stack = state.stack;
1995
1996 while (stack.pop() !== name) {
1997 // do nothing, we just want to consume.
1998 continue;
1999 }
2000 }
2001
2002 function indexSort(obj, field, pattern)
2003 {
2004 obj.sort(function (a, b) {
2005 var avalue = 0;
2006 var bvalue = 0;
2007 var matches;
2008
2009 if (a.hasOwnProperty(field)) {
2010 matches = a[field].match(pattern);
2011 if (matches) {
2012 avalue = Number(matches[1]);
2013 }
2014 }
2015 if (b.hasOwnProperty(field)) {
2016 matches = b[field].match(pattern);
2017 if (matches) {
2018 bvalue = Number(matches[1]);
2019 }
2020 }
2021
2022 return avalue - bvalue;
2023 });
2024 }
2025
2026 function applyTransforms(obj)
2027 {
2028 var p;
2029 var pp;
2030 var subobj;
2031 var transforms = XML_PROPERTY_TRANSFORMS;
2032
2033 for (p in transforms) {
2034 if (obj.hasOwnProperty(p)) {
2035 if (typeof (transforms[p]) === 'object') {
2036 // this is a 'complex' property like nic, and has different
2037 // transforms for the sub-objects
2038 for (pp in transforms[p]) {
2039 for (subobj in obj[p]) {
2040 if (obj[p][subobj].hasOwnProperty(pp)) {
2041 obj[p][subobj][pp] =
2042 transforms[p][pp](obj[p][subobj][pp]);
2043 }
2044 }
2045 }
2046 } else { // function
2047 obj[p] = transforms[p](obj[p]);
2048 }
2049 }
2050 }
2051 }
2052
2053 // This function parses the zone XML file at /etc/zones/<zonename>.xml and adds
2054 // the VM properties to a new object.
2055 function getVmobj(zonename, preload_data, options, callback)
2056 {
2057 var filename = '/etc/zones/' + zonename + '.xml';
2058 var log;
2059 var parser = new expat.Parser('UTF-8');
2060
2061 assert(options.log, 'no logger passed to getVmobj()');
2062 log = options.log;
2063
2064 fs.readFile(filename, function (error, data) {
2065 var allowed;
2066 var disk;
2067 var dsinfo;
2068 var fields;
2069 var nic;
2070 var obj = {};
2071 var state = {};
2072
2073 if (error) {
2074 callback(error);
2075 return;
2076 }
2077
2078 state.obj = obj;
2079 parser.on('startElement', function (name, attrs) {
2080 return startElement(name, attrs, state, log);
2081 });
2082 parser.on('endElement', function (name) {
2083 return endElement(name, state);
2084 });
2085
2086 if (!parser.parse(data.toString())) {
2087 throw new Error('There are errors in your xml file: '
2088 + parser.getError());
2089 }
2090
2091 // now that we know which brand we are, find out what we're allowed.
2092 allowed = BRAND_OPTIONS[obj.brand].allowed_properties;
2093
2094 // replace obj.networks with array of nics.
2095 obj.nics = [];
2096 for (nic in obj.networks) {
2097 obj.nics.push(obj.networks[nic]);
2098 }
2099 delete obj.networks;
2100
2101 // replace obj.devices with array of disks.
2102 if (allowed.hasOwnProperty('disks')) {
2103 obj.disks = [];
2104 for (disk in obj.devices) {
2105 obj.disks.push(obj.devices[disk]);
2106 }
2107 }
2108 delete obj.devices;
2109
2110 if (!BRAND_OPTIONS.hasOwnProperty(obj.brand)) {
2111 throw new Error('unable to handle brand ' + obj.brand);
2112 }
2113
2114 if (BRAND_OPTIONS[obj.brand].features.use_vm_autoboot) {
2115 obj.autoboot = obj.vm_autoboot;
2116 delete obj.vm_autoboot;
2117 }
2118
2119 // apply the XML_PROPERTY_TRANSFORMs
2120 applyTransforms(obj);
2121
2122 // probe for some fields on disks if this brand of zone supports them.
2123 if (allowed.hasOwnProperty('disks')
2124 && (allowed.disks.indexOf('create') !== -1)) {
2125
2126 for (disk in obj.disks) {
2127 disk = obj.disks[disk];
2128
2129 if (preload_data.hasOwnProperty('dsinfo')) {
2130 dsinfo = preload_data.dsinfo;
2131 if (dsinfo.hasOwnProperty('mountpoints')
2132 && dsinfo.mountpoints.hasOwnProperty(disk.path)) {
2133
2134 disk.zfs_filesystem = dsinfo.mountpoints[disk.path];
2135 disk.zpool = disk.zfs_filesystem.split('/')[0];
2136 } else {
2137 log.trace('no mountpoint data for ' + disk.path);
2138 }
2139 }
2140 }
2141 }
2142
2143 if (obj.hasOwnProperty('transition')) {
2144 fields = rtrim(obj.transition).split(':');
2145 if (fields.length === 3) {
2146 delete obj.transition;
2147 obj.state = fields[0];
2148 obj.transition_to = fields[1];
2149 obj.transition_expire = fields[2];
2150 } else {
2151 log.debug('getVmobj() ignoring bad value for '
2152 + 'transition "' + obj.transition + '"');
2153 }
2154 }
2155
2156 // sort the disks + nics by index
2157 if (obj.hasOwnProperty('disks')) {
2158 indexSort(obj.disks, 'path', /^.*-disk(\d+)$/);
2159 }
2160 if (obj.hasOwnProperty('nics')) {
2161 indexSort(obj.nics, 'interface', /^net(\d+)$/);
2162 }
2163 if (obj.hasOwnProperty('filesystems')) {
2164 indexSort(obj.filesystems, 'target', /^(.*)$/);
2165 }
2166
2167 callback(null, obj);
2168 });
2169 }
2170
2171 function setQuota(dataset, quota, log, callback)
2172 {
2173 var newval;
2174
2175 assert(log, 'no logger passed to setQuota()');
2176
2177 if (!dataset) {
2178 callback(new Error('Invalid dataset: "' + dataset + '"'));
2179 return;
2180 }
2181
2182 if (quota === 0 || quota === '0') {
2183 newval = 'none';
2184 } else {
2185 newval = quota.toString() + 'g';
2186 }
2187
2188 zfs(['set', 'quota=' + newval, dataset], log, function (err, fds) {
2189 if (err) {
2190 log.error('setQuota() cmd failed: ' + fds.stderr);
2191 callback(new Error(rtrim(fds.stderr)));
2192 } else {
2193 callback();
2194 }
2195 });
2196 }
2197
2198 function cleanDatasetObject(obj)
2199 {
2200 var number_fields = [
2201 'avail',
2202 'available',
2203 'copies',
2204 'creation',
2205 'filesystem_limit',
2206 'quota',
2207 'recsize',
2208 'recordsize',
2209 'refer',
2210 'referenced',
2211 'refquota',
2212 'refreserv',
2213 'refreservation',
2214 'reserv',
2215 'reservation',
2216 'snapshot_limit',
2217 'usedbychildren',
2218 'usedbydataset',
2219 'usedbyrefreservation',
2220 'usedbysnapshots',
2221 'used',
2222 'userrefs',
2223 'utf8only',
2224 'version',
2225 'volblock',
2226 'volblocksize',
2227 'volsize',
2228 'written'
2229 ];
2230
2231 // We should always have mountpoint, dataset and type because we force them
2232 // to be included in zfsList()
2233 assert(obj.hasOwnProperty('mountpoint'), 'cleanDatasetObject('
2234 + JSON.stringify(obj) + '): missing mountpoint');
2235 assert(obj.hasOwnProperty('name'), 'cleanDatasetObject('
2236 + JSON.stringify(obj) + '): missing name');
2237 assert(obj.hasOwnProperty('type'), 'cleanDatasetObject('
2238 + JSON.stringify(obj) + '): missing type');
2239
2240 // convert numeric fields to proper numbers
2241 number_fields.forEach(function (field) {
2242 if (obj.hasOwnProperty(field) && obj[field] !== '-') {
2243 obj[field] = Number(obj[field]);
2244 }
2245 });
2246
2247 if (obj.type === 'volume') {
2248 obj.mountpoint = '/dev/zvol/rdsk/' + obj.name;
2249 } else if (obj.mountpoint === '-' || obj.mountpoint === 'legacy') {
2250 obj.mountpoint = '/' + obj.name;
2251 }
2252 }
2253
2254 function addDatasetResult(fields, types, results, line, log)
2255 {
2256 var dataset;
2257 var field;
2258 var lfields;
2259 var obj;
2260 var snapparts;
2261 var snapobj;
2262
2263 line = trim(line);
2264
2265 if (line.length === 0) {
2266 return;
2267 }
2268
2269 lfields = line.split(/\s+/);
2270
2271 if (lfields.length !== fields.length) {
2272 return;
2273 }
2274
2275 obj = {};
2276
2277 for (field in fields) {
2278 obj[fields[field]] = lfields[field];
2279 }
2280
2281 cleanDatasetObject(obj);
2282
2283 if (!results.hasOwnProperty('datasets')) {
2284 results.datasets = {};
2285 }
2286 if (!results.hasOwnProperty('mountpoints')) {
2287 results.mountpoints = {};
2288 }
2289 if (types.indexOf('snapshot') !== -1 && obj.type === 'snapshot') {
2290 if (!results.hasOwnProperty('snapshots')) {
2291 results.snapshots = {};
2292 }
2293
2294 /*
2295 * For snapshots we store the snapname and optionally creation keyed by
2296 * dataset name So that we can include the list of snapshots for a
2297 * dataset on a VM.
2298 */
2299 snapparts = obj.name.split('@');
2300 assert.equal(snapparts.length, 2);
2301 dataset = snapparts[0];
2302 snapobj = {snapname: snapparts[1], dataset: dataset};
2303 if (!results.snapshots.hasOwnProperty(dataset)) {
2304 results.snapshots[dataset] = [];
2305 }
2306 if (obj.hasOwnProperty('creation')) {
2307 snapobj.created_at = obj.creation;
2308 }
2309 results.snapshots[dataset].push(snapobj);
2310 }
2311
2312 results.datasets[obj.name] = obj;
2313
2314 /*
2315 * snapshots don't have mountpoint that we care about and we don't count
2316 * 'none' as a mountpoint. If we otherwise have a mountpoint that looks like
2317 * a path, we add a pointer from that to the dataset name.
2318 */
2319 if (obj.type !== 'snapshot' && obj.mountpoint[0] === '/') {
2320 /*
2321 * For zoned filesystems (delegated datasets) we don't use mountpoint as
2322 * this can be changed from within the zone and is therefore not
2323 * reliable. Also, when a delegated dataset is assigned but the zone's
2324 * not been booted, the delegated dataset will not have the 'zoned'
2325 * property. So we also check if the name ends in /data.
2326 */
2327 if (obj.hasOwnProperty('zoned') && obj.zoned === 'on') {
2328 // don't add zoned datasets to mountpoints
2329 /*jsl:pass*/
2330 } else if (obj.name.split('/')[2] === 'data') {
2331 // name is /data, skip
2332 /*jsl:pass*/
2333 } else {
2334 // here we have what looks like a normal non-zoned dataset that's
2335 // probably a zoneroot, add to mountpoints mapping.
2336 results.mountpoints[obj.mountpoint] = obj.name;
2337 }
2338 }
2339 }
2340
2341 /*
2342 * Arguments:
2343 *
2344 * 'fields' - should be an array of fields as listed in the zfs(1m) man page.
2345 * 'types' - should be one or more of: filesystem, snapshot, volume.
2346 * 'log' - should be a bunyan logger object.
2347 * 'callback' - will be called with (err, results)
2348 *
2349 * On failure: callback's err will be an Error object, ignore results.
2350 * On success: callback's results is an object with one or more members of:
2351 *
2352 * results.datasets
2353 *
2354 * keyed by dataset name containing the values for the requested fields.
2355 *
2356 * Eg: results.datasets['zones/cores'] === { name: 'zones/cores', ... }
2357 *
2358 * results.mountpoints
2359 *
2360 * keyed by mountpoint with value being dataset name.
2361 *
2362 * Eg: results.mountpoints['/zones/cores'] === 'zones/cores'
2363 *
2364 * results.snapshots
2365 *
2366 * keyed by dataset with value being array of snapname and created_at.
2367 *
2368 * Eg: results.snapshots['/zones/cores'] === ['snap1', ...]
2369 *
2370 * For non-zoned filesystem datasets (these should be the zoneroot datasets),
2371 * you can use mountpoint which comes from zoneadm's "cheap" info and use that
2372 * to get to the dataset and from datasets[dataset] get the info.
2373 *
2374 * For volumes (KVM VM's disks) you can also use mountpoint as we'll set that
2375 * to the block device path and that's available from the devices section of
2376 * the zoneconfig.
2377 *
2378 * For zoned filesystems (delegated datasets) use the dataset name, as the
2379 * mountpoint can be changed from within the zone.
2380 *
2381 */
2382 function zfsList(fields, types, log, callback) {
2383 var args;
2384 var buffer = '';
2385 var lines;
2386 var cmd = '/usr/sbin/zfs';
2387 var req_fields = ['mountpoint', 'name', 'type'];
2388 var results = {};
2389 var zfs_child;
2390
2391 assert(Array.isArray(types));
2392 assert(Array.isArray(fields));
2393 assert(log, 'no logger passed to zfsList()');
2394
2395 // add any missing required fields
2396 req_fields.forEach(function (field) {
2397 if (fields.indexOf(field) === -1) {
2398 fields.push(field);
2399 }
2400 });
2401
2402 args = ['list', '-H', '-p', '-t', types.join(','), '-o', fields.join(',')];
2403
2404 log.debug(cmd + ' ' + args.join(' '));
2405
2406 zfs_child = spawn(cmd, args, {'customFds': [-1, -1, -1]});
2407 log.debug('zfs running with pid ' + zfs_child.pid);
2408
2409 zfs_child.stdout.on('data', function (data) {
2410 var line;
2411
2412 buffer += data.toString();
2413 lines = buffer.split('\n');
2414 while (lines.length > 1) {
2415 line = lines.shift();
2416
2417 // Add this line to results
2418 addDatasetResult(fields, types, results, line, log);
2419 }
2420 buffer = lines.pop();
2421 });
2422
2423 // doesn't take input.
2424 zfs_child.stdin.end();
2425
2426 zfs_child.on('exit', function (code) {
2427 log.debug('zfs process ' + zfs_child.pid + ' exited with code: '
2428 + code);
2429 if (code === 0) {
2430 callback(null, results);
2431 } else {
2432 callback(new Error('zfs exited prematurely with code: ' + code));
2433 }
2434 });
2435 }
2436
2437 /*
2438 * This queue is used to handle zfs list requests. We do this because of OS-1834
2439 * in order to only run one 'zfs list' at a time. If we need to get data from
2440 * 'zfs list', the parameters we want to list are pushed onto this queue. If a
2441 * list is already running with the same parameters, we'll return the output
2442 * from that one when it completes to all the consumers. If there's not one
2443 * running, or the parameters are different, this set of parameters will be
2444 * pushed onto the tail of the queue. The queue is processed serially so long
2445 * as there are active requests.
2446 */
2447 zfs_list_queue = async.queue(function (task, callback) {
2448
2449 var fields = task.fields;
2450 var log = task.log;
2451 var started = Date.now(0);
2452 var types = task.types;
2453
2454 zfsList(fields, types, log, function (err, data) {
2455 var emitter = zfs_list_in_progress[task];
2456
2457 delete zfs_list_in_progress[task];
2458 emitter.emit('result', err, data);
2459 emitter.removeAllListeners('result');
2460
2461 log.debug('zfs list took ' + (Date.now(0) - started) + ' ms');
2462 callback();
2463 });
2464
2465 }, 1);
2466
2467 zfs_list_queue.drain = function () {
2468 // We use the global log here because this queue is not tied to one action.
2469 VM.log.trace('zfs_list_queue is empty');
2470 };
2471
2472 function getZfsList(fields, types, log, callback) {
2473 var sorted_fields;
2474 var sorted_types;
2475 var task;
2476
2477 sorted_fields = fields.slice().sort();
2478 sorted_types = types.slice().sort();
2479
2480 task = {types: sorted_types, fields: sorted_fields, log: log};
2481
2482 try {
2483 zfs_list_in_progress[task].on('result', callback);
2484 } catch (e) {
2485 if ((e instanceof TypeError)
2486 && (!zfs_list_in_progress.hasOwnProperty(task)
2487 || !zfs_list_in_progress[task].hasOwnProperty('on'))) {
2488
2489 zfs_list_in_progress[task] = new EventEmitter();
2490 zfs_list_in_progress[task].on('result', callback);
2491 zfs_list_in_progress[task].setMaxListeners(0);
2492 zfs_list_queue.push(task);
2493
2494 // callback() will get called when 'result' is emitted.
2495 } else {
2496 callback(e);
2497 }
2498 }
2499 }
2500
2501 function loadDatasetInfo(fields, log, callback)
2502 {
2503 var zfs_fields = [];
2504 var zfs_types = [];
2505
2506 assert(log, 'no logger passed to loadDataset()');
2507
2508 function addField(name) {
2509 if (zfs_fields.indexOf(name) === -1) {
2510 zfs_fields.push(name);
2511 }
2512 }
2513
2514 function addType(name) {
2515 if (zfs_types.indexOf(name) === -1) {
2516 zfs_types.push(name);
2517 }
2518 }
2519
2520 if (!fields || fields.length < 1) {
2521 // Default to grabbing everything we might possibly need.
2522 zfs_fields = ['name', 'quota', 'volsize', 'mountpoint', 'type',
2523 'compression', 'recsize', 'volblocksize', 'zoned', 'creation',
2524 'refreservation'];
2525 zfs_types = ['filesystem', 'snapshot', 'volume'];
2526 } else {
2527 if (fields.indexOf('disks') !== -1) {
2528 addField('compression');
2529 addField('volsize');
2530 addField('volblocksize');
2531 addField('refreservation');
2532 addType('volume');
2533 }
2534 if (fields.indexOf('snapshots') !== -1) {
2535 addField('creation');
2536 addType('snapshot');
2537 addType('filesystem');
2538 addType('volume');
2539 }
2540 if (fields.indexOf('create_timestamp') !== -1) {
2541 // We might fall back to creation on the dataset for
2542 // create_timestamp if we have no create-timestamp attr.
2543 addField('creation');
2544 addType('filesystem');
2545 }
2546 if ((fields.indexOf('zfs_root_compression') !== -1)
2547 || (fields.indexOf('zfs_data_compression') !== -1)) {
2548
2549 addField('compression');
2550 addType('filesystem');
2551 }
2552 if ((fields.indexOf('zfs_root_recsize') !== -1)
2553 || (fields.indexOf('zfs_data_recsize') !== -1)) {
2554
2555 addField('recsize');
2556 addType('filesystem');
2557 }
2558 if (fields.indexOf('quota') !== -1) {
2559 addField('quota');
2560 addType('filesystem');
2561 }
2562 // both zpool and zfs_filesystem come from 'name'
2563 if (fields.indexOf('zpool') !== -1
2564 || fields.indexOf('zfs_filesystem') !== -1) {
2565
2566 addField('name');
2567 addType('filesystem');
2568 }
2569 if (zfs_fields.length > 0) {
2570 // we have some fields so we need to zfs, make sure we have name,
2571 // mountpoint and type which we always need if we get anything.
2572 addField('name');
2573 addField('mountpoint');
2574 addField('type');
2575
2576 if (zfs_types.indexOf('filesystem') !== -1) {
2577 // to differentiate between delegated and root filesystems
2578 addField('zoned');
2579 }
2580 } else {
2581 log.debug('no need to call zfs');
2582 callback(null, {
2583 datasets: {},
2584 mountpoints: {},
2585 snapshots: {}
2586 });
2587 return;
2588 }
2589 }
2590
2591 /*
2592 * NOTE:
2593 * in the future, the plan is to have the list of types and fields
2594 * be dynamic based what's actually needed to handle the request.
2595 */
2596
2597 getZfsList(zfs_fields, zfs_types, log, callback);
2598 }
2599
2600 function loadJsonConfig(vmobj, cfg, log, callback)
2601 {
2602 var filename;
2603
2604 assert(log, 'no logger passed to loadJsonConfig()');
2605
2606 if (vmobj.zonepath) {
2607 filename = vmobj.zonepath + '/config/' + cfg + '.json';
2608 log.trace('loadJsonConfig() loading ' + filename);
2609
2610 fs.readFile(filename, function (error, data) {
2611 var json = {};
2612
2613 if (error) {
2614 if (error.code === 'ENOENT') {
2615 log.debug('Skipping nonexistent file ' + filename);
2616 } else {
2617 log.error(error,
2618 'loadJsonConfig() failed to load ' + filename);
2619 callback(error, {});
2620 return;
2621 }
2622 } else {
2623 try {
2624 json = JSON.parse(data.toString());
2625 } catch (e) {
2626 json = {};
2627 }
2628 }
2629
2630 callback(null, json);
2631 });
2632 } else {
2633 callback(null, {});
2634 }
2635 }
2636
2637 /*
2638 * This preloads some data for us that comes from commands which output for
2639 * *all* VMs. This allows us to just run these (expensive) commands once
2640 * instead of having to run them for each VM.
2641 *
2642 */
2643 function preloadZoneData(uuid, options, callback)
2644 {
2645 var data = {};
2646 var log;
2647
2648 assert(options.log, 'no logger passed to preloadZoneData()');
2649 log = options.log;
2650
2651 // NOTE: uuid can be null, in which case we get data for all VMs.
2652
2653 async.series([
2654 function (cb) {
2655 // We always do this (calls `zoneadm list -vc`) since we always
2656 // need to know which zones exist.
2657 getZoneRecords(uuid, log, function (err, records) {
2658 if (!err) {
2659 data.records = records;
2660 }
2661 cb(err);
2662 });
2663 }, function (cb) {
2664 var fields;
2665
2666 if (options.hasOwnProperty('fields')) {
2667 fields = options.fields;
2668 } else {
2669 fields = [];
2670 }
2671
2672 loadDatasetInfo(fields, log, function (err, dsinfo) {
2673 if (!err) {
2674 data.dsinfo = dsinfo;
2675 }
2676 cb(err);
2677 });
2678 }, function (cb) {
2679 if (options.hasOwnProperty('fields')
2680 && (options.fields.indexOf('server_uuid') === -1
2681 && options.fields.indexOf('datacenter_name') === -1
2682 && options.fields.indexOf('headnode_id') === -1)) {
2683
2684 // we don't need any fields that come from sysinfo.
2685 log.debug('no need to call sysinfo, no sysinfo fields in list');
2686 data.sysinfo = {};
2687 cb();
2688 return;
2689 }
2690
2691 VM.getSysinfo([], {log: log}, function (err, sysinfo) {
2692 if (!err) {
2693 data.sysinfo = sysinfo;
2694 }
2695 cb(err);
2696 });
2697 }, function (cb) {
2698 var u;
2699 var uuids = [];
2700
2701 if (options.hasOwnProperty('fields')
2702 && options.fields.indexOf('pid') === -1) {
2703
2704 log.debug('no need to check PID files, PID not in field list');
2705 cb();
2706 return;
2707 }
2708
2709 // get the PID values from running KVM VMs
2710
2711 for (u in data.records) {
2712 uuids.push(u);
2713 }
2714 async.forEachSeries(uuids, function (z_uuid, zcb) {
2715 var filename;
2716 var z = data.records[z_uuid];
2717
2718 // NOTE: z.state here is equivalent to zone_state not state.
2719 if (z && BRAND_OPTIONS[z.brand].hasOwnProperty('features')
2720 && BRAND_OPTIONS[z.brand].features.pid_file
2721 && z.state === 'running') {
2722
2723 // ensure pid_file is safe
2724 try {
2725 assertSafeZonePath(path.join(z.zonepath, '/root'),
2726 BRAND_OPTIONS[z.brand].features.pid_file,
2727 {type: 'file', enoent_ok: true});
2728 } catch (e) {
2729 // We log an error here, but not being able to get
2730 // the PID for one broken machine should not impact the
2731 // ability to get a list of all machines, so we just
2732 // skip adding a PID and log an error here.
2733 log.error(e, 'Unsafe path for ' + z.uuid + ' cannot '
2734 + 'check for PID file: ' + e.message);
2735 zcb();
2736 return;
2737 }
2738
2739 filename = path.join(z.zonepath, 'root',
2740 BRAND_OPTIONS[z.brand].features.pid_file);
2741 log.debug('checking for ' + filename);
2742
2743 fs.readFile(filename,
2744 function (error, filedata) {
2745
2746 var pid;
2747
2748 if (!error) {
2749 pid = Number(trim(filedata.toString()));
2750 if (pid > 0) {
2751 z.pid = pid;
2752 log.debug('found PID ' + pid + ' for '
2753 + z.uuid);
2754 }
2755 }
2756 if (error && error.code === 'ENOENT') {
2757 // don't return error in this case because it just
2758 // didn't exist
2759 log.debug('no PID file for ' + z.uuid);
2760 zcb();
2761 } else {
2762 zcb(error);
2763 }
2764 });
2765 } else {
2766 zcb();
2767 }
2768 }, function (err) {
2769 cb(err);
2770 });
2771 }
2772 ], function (err, res) {
2773 log.trace('leaving preloadZoneData()');
2774 callback(err, data);
2775 });
2776 }
2777
2778 function getZoneRecords(uuid, log, callback)
2779 {
2780 var args = [];
2781 var buffer = '';
2782 var cmd = '/usr/sbin/zoneadm';
2783 var line_count = 0;
2784 var lines;
2785 var results = {};
2786 var zadm;
2787 var zadm_stderr = '';
2788
2789 assert(log, 'no logger passed to getZoneRecords()');
2790
2791 if (uuid) {
2792 // this gives us zone info if uuid is *either* a zonename or uuid
2793 if (isUUID(uuid)) {
2794 args.push('-z');
2795 args.push(uuid);
2796 args.push('-u');
2797 args.push(uuid);
2798 } else {
2799 args.push('-z');
2800 args.push(uuid);
2801 }
2802 }
2803 args.push('list');
2804 args.push('-p');
2805 if (!uuid) {
2806 args.push('-c');
2807 }
2808
2809 log.debug(cmd + ' ' + args.join(' '));
2810
2811 zadm = spawn(cmd, args, {'customFds': [-1, -1, -1]});
2812 log.debug('zoneadm running with PID ' + zadm.pid);
2813
2814 zadm.stderr.on('data', function (data) {
2815 zadm_stderr += data.toString();
2816 });
2817
2818 zadm.stdout.on('data', function (data) {
2819 var fields;
2820 var line;
2821 var obj;
2822
2823 buffer += data.toString();
2824 lines = buffer.split('\n');
2825 while (lines.length > 1) {
2826 line = lines.shift();
2827 line_count++;
2828 fields = rtrim(line).split(':');
2829 if (fields.length === 8 && fields[1] !== 'global') {
2830 obj = {
2831 'zoneid': Number(fields[0]),
2832 'zonename': fields[1],
2833 'state': fields[2],
2834 'zonepath': fields[3],
2835 'uuid': fields[4],
2836 'brand': fields[5],
2837 'ip_type': fields[6]
2838 };
2839 log.trace('loaded: ' + JSON.stringify(obj));
2840 // XXX zones in some states have no uuid. We should either fix
2841 // that or use zonename for those.
2842 results[obj.uuid] = obj;
2843 } else if (line.replace(/ /g, '').length > 0) {
2844 log.debug('getZoneRecords(' + uuid + ') ignoring: ' + line);
2845 }
2846 }
2847 buffer = lines.pop();
2848 });
2849
2850 // doesn't take input.
2851 zadm.stdin.end();
2852
2853 zadm.on('close', function (code) {
2854 var errmsg;
2855 var new_err;
2856
2857 log.debug('zoneadm process ' + zadm.pid + ' exited with code: '
2858 + code + ' (' + line_count + ' lines to stdout)');
2859 if (code === 0) {
2860 callback(null, results);
2861 } else {
2862 errmsg = rtrim(zadm_stderr);
2863 new_err = new Error(errmsg);
2864 if (errmsg.match(/No such zone configured$/)) {
2865 // not existing isn't always a problem (eg. existence check)
2866 new_err.code = 'ENOENT';
2867 } else {
2868 log.error({err: new_err, stderr: zadm_stderr},
2869 'getZoneRecords() zoneadm "' + args.join(',') + '" failed');
2870 }
2871 callback(new_err);
2872 return;
2873 }
2874 });
2875 }
2876
2877 exports.flatten = function (vmobj, key)
2878 {
2879 var index;
2880 var tokens = key.split('.');
2881
2882 // NOTE: VM.flatten() currently doesn't produce any logs
2883
2884 if (tokens.length === 3
2885 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(tokens[0]) !== -1) {
2886
2887 if (!vmobj.hasOwnProperty(tokens[0])) {
2888 return undefined;
2889 }
2890 if (!vmobj[tokens[0]].hasOwnProperty(tokens[1])) {
2891 return undefined;
2892 }
2893 return vmobj[tokens[0]][tokens[1]][tokens[2]];
2894 }
2895
2896 if (tokens.length === 2
2897 && VM.FLATTENABLE_HASH_KEYS.indexOf(tokens[0]) !== -1) {
2898
2899 if (!vmobj.hasOwnProperty(tokens[0])) {
2900 return undefined;
2901 }
2902 return vmobj[tokens[0]][tokens[1]];
2903 }
2904
2905 if (tokens.length === 2
2906 && VM.FLATTENABLE_ARRAYS.indexOf(tokens[0]) !== -1) {
2907
2908 index = Number(tokens[1]);
2909
2910 if (!vmobj.hasOwnProperty(tokens[0])) {
2911 return undefined;
2912 }
2913
2914 if (index === NaN || index < 0
2915 || !vmobj[tokens[0]].hasOwnProperty(index)) {
2916
2917 return undefined;
2918 }
2919 return vmobj[tokens[0]][index];
2920 }
2921
2922 return vmobj[key];
2923 };
2924
2925 function getLastModified(vmobj, log)
2926 {
2927 var files = [];
2928 var file;
2929 var stat;
2930 var timestamp = 0;
2931
2932 assert(log, 'no logger passed to getLastModified()');
2933
2934 if (vmobj.zonepath) {
2935 files.push(path.join(vmobj.zonepath, '/config/metadata.json'));
2936 files.push(path.join(vmobj.zonepath, '/config/routes.json'));
2937 files.push(path.join(vmobj.zonepath, '/config/tags.json'));
2938 } else {
2939 log.debug('getLastModified() no zonepath!');
2940 }
2941
2942 if (vmobj.hasOwnProperty('zonename')) {
2943 files.push('/etc/zones/' + vmobj.zonename + '.xml');
2944 } else {
2945 log.debug('getLastModified() no zonename!');
2946 }
2947
2948 for (file in files) {
2949 file = files[file];
2950 try {
2951 stat = fs.statSync(file);
2952 if (stat.isFile()) {
2953 if ((timestamp === 0) || (Date.parse(stat.mtime) > timestamp)) {
2954 timestamp = Date.parse(stat.mtime);
2955 }
2956 }
2957 } catch (e) {
2958 if (e.code !== 'ENOENT') {
2959 log.error(e, 'Unable to get timestamp for "' + file + '":'
2960 + e.message);
2961 }
2962 }
2963 }
2964
2965 return ((new Date(timestamp)).toISOString());
2966 }
2967
2968 function loadVM(uuid, data, options, callback)
2969 {
2970 var e;
2971 var info;
2972 var log;
2973
2974 assert(options.log, 'no logger passed to loadVM()');
2975 log = options.log;
2976
2977 // XXX need to always have data when we get here
2978 info = data.records[uuid];
2979
2980 if (!info) {
2981 e = new Error('VM.load() empty info when getting record '
2982 + 'for vm ' + uuid);
2983 log.error(e);
2984 callback(e);
2985 return;
2986 }
2987
2988 getVmobj(info.zonename, data, options, function (err, vmobj) {
2989 if (err) {
2990 callback(err);
2991 return;
2992 }
2993
2994 function wantField(field) {
2995 if (options.hasOwnProperty('fields')
2996 && options.fields.indexOf(field) === -1) {
2997
2998 return false;
2999 }
3000
3001 return true;
3002 }
3003
3004 // We got some bits from `zoneadm list` as <info> here, and since we
3005 // already got that data, adding it to the object here is cheap. We also
3006 // need some of these properties to be able to get others later, so we
3007 // add them all now. If they're unwanted they'll be removed from the
3008 // final object.
3009 vmobj.uuid = info.uuid;
3010 vmobj.zone_state = info.state;
3011
3012 // In the case of 'configured' zones, we might only have zonename
3013 // because uuid isn't set yet. Because of that case, we set uuid
3014 // to zonename if it is in UUID form.
3015 if ((!vmobj.uuid || vmobj.uuid.length === 0)
3016 && isUUID(vmobj.zonename)) {
3017
3018 vmobj.uuid = vmobj.zonename;
3019 }
3020
3021 // These ones we never need elsewhere, so don't bother adding if we
3022 // don't need to.
3023 if (wantField('zoneid') && info.zoneid !== '-') {
3024 vmobj.zoneid = info.zoneid;
3025 }
3026
3027 if (wantField('pid') && info.pid) {
3028 vmobj.pid = info.pid;
3029 }
3030
3031 // find when we last modified this VM
3032 if (wantField('last_modified')) {
3033 vmobj.last_modified = getLastModified(vmobj, log);
3034 }
3035
3036 // If we want resolvers, (eg. OS-2194) we always add the array here
3037 // so you can tell that the resolvers are explicitly not set.
3038 if (wantField('resolvers') && !vmobj.hasOwnProperty('resolvers')) {
3039 vmobj.resolvers = [];
3040 }
3041
3042 // sysinfo has server_uuid and potentially some DC info
3043 if (data.hasOwnProperty('sysinfo')) {
3044 if (wantField('server_uuid')
3045 && data.sysinfo.hasOwnProperty('UUID')) {
3046
3047 vmobj.server_uuid = data.sysinfo.UUID;
3048 }
3049 if (wantField('datacenter_name')
3050 && data.sysinfo.hasOwnProperty('Datacenter Name')) {
3051
3052 vmobj.datacenter_name = data.sysinfo['Datacenter Name'];
3053 }
3054 if (wantField('headnode_id')
3055 && data.sysinfo.hasOwnProperty('Headnode ID')) {
3056
3057 vmobj.headnode_id = data.sysinfo['Headnode ID'];
3058 }
3059 }
3060
3061 // state could already be set here if it was overriden by a transition
3062 // that's in progress. So we only change if that's not the case.
3063 if (wantField('state')) {
3064 if (!vmobj.hasOwnProperty('state')) {
3065 if (info.state === 'installed') {
3066 vmobj.state = 'stopped';
3067 } else {
3068 vmobj.state = info.state;
3069 }
3070 }
3071
3072 // If the zone has the 'failed' property it doesn't matter what
3073 // other state it might be in, we list its state as 'failed'.
3074 if (vmobj.failed) {
3075 vmobj.state = 'failed';
3076 }
3077 }
3078
3079 async.series([
3080 function (cb) {
3081 if (!wantField('customer_metadata')
3082 && !wantField('internal_metadata')) {
3083
3084 cb();
3085 return;
3086 }
3087
3088 loadJsonConfig(vmobj, 'metadata', log,
3089 function (error, metadata) {
3090 if (error) {
3091 // when zone_state is 'incomplete' we could be
3092 // deleting it in which case metadata may already
3093 // be gone, ignore failure to load mdata when
3094 // 'incomplete' because of this.
3095 if (vmobj.zone_state === 'incomplete') {
3096 log.debug(error, 'zone is in state incomplete '
3097 + 'ignoring error: ' + error.message);
3098 } else {
3099 cb(error);
3100 return;
3101 }
3102 }
3103
3104 if (wantField('customer_metadata')) {
3105 if (metadata.hasOwnProperty('customer_metadata')) {
3106 vmobj.customer_metadata
3107 = metadata.customer_metadata;
3108 } else {
3109 vmobj.customer_metadata = {};
3110 }
3111 }
3112
3113 if (wantField('internal_metadata')) {
3114 if (metadata.hasOwnProperty('internal_metadata')) {
3115 vmobj.internal_metadata
3116 = metadata.internal_metadata;
3117 } else {
3118 vmobj.internal_metadata = {};
3119 }
3120 }
3121
3122 cb();
3123 });
3124 }, function (cb) {
3125 if (!wantField('tags')) {
3126 cb();
3127 return;
3128 }
3129
3130 loadJsonConfig(vmobj, 'tags', log, function (error, tags) {
3131 if (error) {
3132 // when zone_state is 'incomplete' we could be deleting
3133 // it in which case metadata may already be gone, ignore
3134 // failure to load mdata when 'incomplete' because of
3135 // this.
3136 if (vmobj.zone_state === 'incomplete') {
3137 log.debug(error, 'zone is in state incomplete '
3138 + 'ignoring error: ' + error.message);
3139 } else {
3140 cb(error);
3141 return;
3142 }
3143 }
3144 vmobj.tags = tags;
3145 cb();
3146 });
3147 }, function (cb) {
3148 if (!wantField('routes')) {
3149 cb();
3150 return;
3151 }
3152
3153 loadJsonConfig(vmobj, 'routes', log, function (error, routes) {
3154 if (error) {
3155 // same as tags above, if zone_state is 'incomplete'
3156 // we could be a file that's already gone
3157 if (vmobj.zone_state === 'incomplete') {
3158 log.debug(error, 'zone is in state incomplete '
3159 + 'ignoring error: ' + error.message);
3160 } else {
3161 cb(error);
3162 return;
3163 }
3164 }
3165 vmobj.routes = routes;
3166 cb();
3167 });
3168 }, function (cb) {
3169 var dsinfo;
3170 var dsname;
3171 var dsobj;
3172 var d;
3173 var delegated;
3174 var disk;
3175 var ds;
3176 var filesys;
3177 var friendly_snap;
3178 var friendly_snapshots = [];
3179 var matches;
3180 var raw_snapshots = [];
3181 var snap;
3182 var snap_time;
3183
3184 // local alias, data.dsinfo should include all the info about
3185 // this VM's zoneroot that we care about here.
3186 dsinfo = data.dsinfo;
3187
3188 if (dsinfo.hasOwnProperty('mountpoints')
3189 && dsinfo.hasOwnProperty('datasets')
3190 && dsinfo.mountpoints.hasOwnProperty(vmobj.zonepath)) {
3191
3192 dsname = dsinfo.mountpoints[vmobj.zonepath];
3193 dsobj = dsinfo.datasets[dsname];
3194
3195 /* dsobj.quota is in bytes, we want GiB for vmobj.quota */
3196 if (wantField('quota') && dsobj.hasOwnProperty('quota')) {
3197 vmobj.quota = (dsobj.quota / (1024 * 1024 * 1024));
3198 log.trace('found quota "' + vmobj.quota + '" for '
3199 + vmobj.uuid);
3200 }
3201
3202 if (wantField('create_timestamp')
3203 && !vmobj.hasOwnProperty('create_timestamp')
3204 && dsobj.hasOwnProperty('creation')) {
3205
3206 log.debug('VM has no create_timestamp, using creation '
3207 + 'from ' + dsobj.name);
3208 vmobj.create_timestamp =
3209 (new Date(dsobj.creation * 1000)).toISOString();
3210 }
3211
3212 if (wantField('zfs_root_compression')
3213 && dsobj.hasOwnProperty('compression')
3214 && (dsobj.compression !== 'off')) {
3215
3216 vmobj.zfs_root_compression = dsobj.compression;
3217 }
3218
3219 if (wantField('zfs_root_recsize')
3220 && dsobj.hasOwnProperty('recsize')) {
3221
3222 vmobj.zfs_root_recsize = dsobj.recsize;
3223 }
3224
3225 // Always add zfs_filesystem if we can because it's needed
3226 // to find other properties such as delegated_dataset.
3227 vmobj.zfs_filesystem = dsobj.name;
3228
3229 if (wantField('snapshots')
3230 && dsinfo.hasOwnProperty('snapshots')
3231 && dsinfo.snapshots
3232 .hasOwnProperty(vmobj.zfs_filesystem)) {
3233
3234 raw_snapshots = raw_snapshots.concat(
3235 dsinfo.snapshots[vmobj.zfs_filesystem]);
3236 }
3237
3238 log.trace('found dataset "' + vmobj.zfs_filesystem
3239 + '" for ' + vmobj.uuid);
3240 } else {
3241 log.trace('no dsinfo for ' + vmobj.uuid + ': '
3242 + vmobj.zonepath);
3243 }
3244
3245 // delegated datasets are keyed on the dataset name instead of
3246 // mountpoint, since mountpoint can change in a zone.
3247 if (vmobj.hasOwnProperty('zfs_filesystem')) {
3248 delegated = vmobj.zfs_filesystem + '/data';
3249 if (dsinfo.datasets.hasOwnProperty(delegated)) {
3250 dsobj = dsinfo.datasets[delegated];
3251
3252 if (dsobj.hasOwnProperty('compression')
3253 && (dsobj.compression !== 'off')) {
3254
3255 vmobj.zfs_data_compression = dsobj.compression;
3256 }
3257 if (dsobj.hasOwnProperty('recsize')) {
3258 vmobj.zfs_data_recsize = dsobj.recsize;
3259 }
3260
3261 // If there are snapshots for this dataset, add them
3262 if (DISABLED) {
3263 // XXX currently only support snapshot on
3264 // zfs_filesystem
3265 if (dsinfo.hasOwnProperty('snapshots')
3266 && dsinfo.snapshots.hasOwnProperty(delegated)) {
3267
3268 raw_snapshots = raw_snapshots
3269 .concat(dsinfo.snapshots[delegated]);
3270 }
3271 }
3272 } else {
3273 log.trace('no dsinfo for delegated dataset: '
3274 + delegated);
3275 }
3276
3277 vmobj.zpool =
3278 vmobj.zfs_filesystem.split('/')[0];
3279 }
3280
3281 if (wantField('disks') && vmobj.hasOwnProperty('disks')) {
3282 for (d in vmobj.disks) {
3283 d = vmobj.disks[d];
3284 if (d.hasOwnProperty('path')
3285 && dsinfo.mountpoints.hasOwnProperty(d.path)) {
3286
3287 dsname = dsinfo.mountpoints[d.path];
3288 dsobj = dsinfo.datasets[dsname];
3289
3290 if (dsobj.hasOwnProperty('volsize')) {
3291
3292 /* dsobj.volsize is in bytes, we want MiB */
3293 d.size = (dsobj.volsize / (1024 * 1024));
3294 log.debug('found size=' + d.size + ' for '
3295 + JSON.stringify(d));
3296 }
3297 if (dsobj.hasOwnProperty('compression')) {
3298 d.compression = dsobj.compression;
3299 }
3300 if (dsobj.hasOwnProperty('refreservation')) {
3301 /* dsobj.refreservation is in bytes, want MiB */
3302 d.refreservation
3303 = (dsobj.refreservation / (1024 * 1024));
3304 log.debug('found refreservation='
3305 + d.refreservation + ' for '
3306 + JSON.stringify(d));
3307 }
3308 if (dsobj.hasOwnProperty('volblocksize')) {
3309 d.block_size = dsobj.volblocksize;
3310 }
3311
3312 // If there are snapshots for this dataset, add them
3313 // to the list.
3314 if (DISABLED) {
3315 // XXX currently only support snapshots on
3316 // zfs_filesystem
3317 if (dsinfo.hasOwnProperty('snapshots')
3318 && dsinfo.snapshots.hasOwnProperty(
3319 d.zfs_filesystem)) {
3320
3321 raw_snapshots = raw_snapshots.concat(dsinfo
3322 .snapshots[d.zfs_filesystem]);
3323 }
3324 }
3325 } else if (d.hasOwnProperty('path')) {
3326 d.missing = true;
3327 } else {
3328 log.warn('no dsinfo and no path for '
3329 + JSON.stringify(d));
3330 }
3331 }
3332 }
3333
3334 // snapshots here is the raw list of snapshots, now we need to
3335 // convert it to the "friendly" list of snapshots.
3336 if (wantField('snapshots')) {
3337 for (snap in raw_snapshots) {
3338 snap = raw_snapshots[snap];
3339
3340 matches = snap.snapname.match(/^vmsnap-(.*)$/);
3341 if (matches && matches[1]) {
3342 friendly_snap = {name: matches[1]};
3343 if (snap.hasOwnProperty('created_at')) {
3344 snap_time
3345 = new Date(snap.created_at * 1000); // in ms
3346 friendly_snap.created_at
3347 = snap_time.toISOString();
3348 }
3349 friendly_snapshots.push(friendly_snap);
3350 } else {
3351 log.debug('ignoring unfriendly ' + snap.snapname);
3352 continue;
3353 }
3354 }
3355 // sort the snapshots with newest first.
3356 friendly_snapshots.sort(function (a, b) {
3357 if (a.created_at > b.created_at) {
3358 return -1;
3359 }
3360 if (a.created_at < b.created_at) {
3361 return 1;
3362 }
3363 return 0; // equal
3364 });
3365 vmobj.snapshots = friendly_snapshots;
3366 }
3367
3368 if (vmobj.state === 'receiving') {
3369 vmobj.missing = { 'datasets': [], 'disks': [],
3370 'filesystems': [] };
3371 if (!fs.existsSync(vmobj.zonepath)) {
3372 vmobj.missing.datasets.push(vmobj.zonepath.substr(1));
3373 }
3374 for (ds in vmobj.datasets) {
3375 ds = vmobj.datasets[ds];
3376 vmobj.missing.datasets.push(ds);
3377 }
3378 for (filesys in vmobj.filesystems) {
3379 filesys = vmobj.filesystems[filesys];
3380 if (filesys.hasOwnProperty('source')) {
3381 vmobj.missing.filesystems.push(filesys.source);
3382 }
3383 }
3384 for (disk in vmobj.disks) {
3385 disk = vmobj.disks[disk];
3386 if (disk.hasOwnProperty('missing')) {
3387 vmobj.missing.disks.push(disk.path);
3388 }
3389 }
3390 }
3391
3392 cb();
3393 }
3394 ], function (error) {
3395 callback(error, vmobj);
3396 });
3397
3398 });
3399 }
3400
3401 exports.load = function (uuid, options, callback)
3402 {
3403 var log;
3404 var load_opts = {};
3405
3406 // This is a wrapper so that other internal functions here (such as lookup)
3407 // can do smart things like check the quota for each VM with a separate call
3408 // to zfs get.
3409
3410 // options is optional
3411 if (arguments.length === 2) {
3412 callback = arguments[1];
3413 options = {};
3414 }
3415
3416 ensureLogging(false);
3417 if (options.hasOwnProperty('log')) {
3418 log = options.log;
3419 } else {
3420 log = VM.log.child({action: 'load', vm: uuid});
3421 }
3422
3423 load_opts.log = log;
3424 if (options.hasOwnProperty('fields')) {
3425 load_opts.fields = options.fields;
3426 }
3427
3428 preloadZoneData(uuid, load_opts, function (error, data) {
3429 if (error) {
3430 if (options.missing_ok && error.code === 'ENOENT') {
3431 // we're expecting the zone to be gone in this case (eg. delete)
3432 log.debug('VM ' + uuid + ' does not exist (as expected)');
3433 } else {
3434 log.error(error, 'VM.load() failed to get zone record'
3435 + ' for ' + uuid);
3436 }
3437 callback(error);
3438 } else {
3439 loadVM(uuid, data, load_opts, function (e, vmobj) {
3440 if (e) {
3441 callback(e);
3442 return;
3443 }
3444
3445 if (load_opts.hasOwnProperty('fields')) {
3446 // clean out unwanted fields
3447 Object.keys(vmobj).forEach(function (key) {
3448 if (options.fields.indexOf(key) === -1) {
3449 delete vmobj[key];
3450 }
3451 });
3452 }
3453 callback(null, vmobj);
3454 });
3455 }
3456 });
3457 };
3458
3459 function fixMac(str)
3460 {
3461 var fixed = [];
3462 var octet;
3463 var octets = str.split(':');
3464
3465 for (octet in octets) {
3466 if (octets.hasOwnProperty(octet)) {
3467 octet = parseInt(octets[octet], 16);
3468 if (octet === 'nan') {
3469 octet = 0;
3470 }
3471 fixed.push(sprintf('%02x', octet));
3472 }
3473 }
3474
3475 return fixed.join(':');
3476 }
3477
3478 // zonecfg requires removing leading 0's in MACs like 01:02:03:04:05:06
3479 // This function takes a MAC in normal form and puts it in the goofy form
3480 // zonecfg wants.
3481 function ruinMac(mac)
3482 {
3483 var part;
3484 var parts;
3485 var out = [];
3486
3487 parts = mac.split(':');
3488
3489 for (part in parts) {
3490 part = ltrim(parts[part], '0');
3491 if (part.length === 0) {
3492 part = '0';
3493 }
3494 out.push(part);
3495 }
3496
3497 return (out.join(':'));
3498 }
3499
3500 function matcher(zone, search)
3501 {
3502 var fields;
3503 var found;
3504 var i;
3505 var key;
3506 var parameters_matched = 0;
3507 var regex;
3508 var target;
3509
3510 function find_match(k, targ) {
3511 var value = VM.flatten(zone, k);
3512
3513 if (!regex && k.match(/^nics\..*\.mac$/)) {
3514 // Fix for broken SmartOS MAC format
3515 targ = fixMac(targ);
3516 }
3517
3518 if (regex && (value !== undefined) && value.toString().match(targ)) {
3519 found = true;
3520 } else if ((value !== undefined)
3521 && value.toString() === targ.toString()) {
3522 found = true;
3523 }
3524 }
3525
3526 for (key in search) {
3527 found = false;
3528 regex = false;
3529
3530 target = search[key];
3531 if (target[0] === '~') {
3532 regex = true;
3533 target = new RegExp(target.substr(1), 'i');
3534 }
3535
3536 fields = key.split('.');
3537 if (fields.length === 3 && fields[1] === '*'
3538 && zone.hasOwnProperty(fields[0])
3539 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(fields[0]) !== -1) {
3540
3541 // Special case: for eg. nics.*.ip, we want to loop through all nics
3542 for (i = 0; i < zone[fields[0]].length; i++) {
3543 fields[1] = i;
3544 find_match(fields.join('.'), target);
3545 }
3546 } else {
3547 find_match(key, target);
3548 }
3549
3550 if (!found) {
3551 return false;
3552 } else {
3553 parameters_matched++;
3554 }
3555 }
3556
3557 if (parameters_matched > 0) {
3558 // we would have returned false from the loop had any parameters not
3559 // matched and we had at least one that did.
3560 return true;
3561 }
3562
3563 return false;
3564 }
3565
3566 exports.lookup = function (search, options, callback)
3567 {
3568 var log;
3569 var key;
3570 var matches;
3571 var need_fields = [];
3572 var preload_opts = {};
3573 var quick_ok = true;
3574 var results = [];
3575 var transform;
3576
3577 // options is optional
3578 if (arguments.length === 2) {
3579 callback = arguments[1];
3580 options = {};
3581 }
3582
3583 ensureLogging(false);
3584 if (options.hasOwnProperty('log')) {
3585 log = options.log;
3586 } else {
3587 log = VM.log.child({action: 'lookup', search: search});
3588 }
3589
3590 // XXX the 'transform' option is not intended to be public yet and should
3591 // only be used by tools willing to be rewritten if this is removed or
3592 // changed.
3593 if (options.hasOwnProperty('transform')) {
3594 transform = options.transform;
3595 }
3596
3597 // keep separate variable because we can have some fields we add below that
3598 // we need for searching, but shouldn't be in the output.
3599 if (options.hasOwnProperty('fields')) {
3600 need_fields = options.fields.slice(0);
3601 }
3602
3603 for (key in search) {
3604 // To be able to search on a field, that field needs to be added to
3605 // the objects, if user requested a set of fields missing the one
3606 // they're searching for, add it.
3607 matches = key.match(/^([^.]+)\./);
3608 if (matches) {
3609 if (need_fields.indexOf(matches[1]) == -1) {
3610 need_fields.push(matches[1]);
3611 }
3612 } else {
3613 if (need_fields.indexOf(key) == -1) {
3614 need_fields.push(key);
3615 }
3616 }
3617 }
3618
3619 // If all the keys we're searching for are in the QUICK_LOOKUP data, we
3620 // don't need the full zone records to locate the VMs we're interested in.
3621 for (key in need_fields) {
3622 if (QUICK_LOOKUP.indexOf(key) === -1) {
3623 quick_ok = false;
3624 }
3625 }
3626
3627 preload_opts.log = log;
3628 if (options.hasOwnProperty('fields')) {
3629 preload_opts.fields = need_fields;
3630 }
3631
3632 // This is used when you've specified fields to remove those that might
3633 // have been added as a group but are not wanted, or were added as
3634 // dependencies for looking up wanted fields, or for search.
3635 function filterFields(res) {
3636 res.forEach(function (result) {
3637 Object.keys(result).forEach(function (k) {
3638 if (options.fields.indexOf(k) === -1) {
3639 delete result[k];
3640 }
3641 });
3642 });
3643 }
3644
3645 preloadZoneData(null, preload_opts, function (err, data) {
3646 var records = data.records;
3647 var uuids = [];
3648
3649 if (err) {
3650 callback(err);
3651 return;
3652 }
3653
3654 if (quick_ok) {
3655 var full_results = [];
3656 var load_opts = {};
3657 var match;
3658 var regex;
3659 var source;
3660 var target;
3661 var u;
3662 var z;
3663
3664 if (err) {
3665 callback(err);
3666 return;
3667 }
3668 for (z in records) {
3669 z = records[z];
3670 match = true;
3671 for (key in search) {
3672 regex = false;
3673 // force field type to string so that earlier transformed
3674 // number fields get back their match method and the
3675 // strict not-equal operator will work on number lookups
3676 source = '' + z[key];
3677 target = search[key];
3678 if (target[0] === '~') {
3679 target = new RegExp(target.substr(1), 'i');
3680 regex = true;
3681 }
3682 if (regex && !source.match(target)) {
3683 match = false;
3684 } else if (!regex && (source !== search[key])) {
3685 match = false;
3686 }
3687 }
3688 if (match && z.uuid) {
3689 results.push(z.uuid);
3690 }
3691 }
3692
3693 load_opts.log = log;
3694 if (options.hasOwnProperty('fields') && need_fields.length > 0) {
3695 // we have a specific set of fields we want to grab
3696 load_opts.fields = need_fields;
3697 } else if (!options.full) {
3698 // we don't need all the data so what we already got is enough
3699 if (options.hasOwnProperty('fields')) {
3700 filterFields(results);
3701 }
3702
3703 callback(null,
3704 results.filter(function (res) {
3705 if (typeof (res) === 'object') {
3706 return (Object.keys(res).length > 0);
3707 } else {
3708 return (true);
3709 }
3710 })
3711 );
3712 return;
3713 }
3714
3715 function expander(uuid, cb) {
3716 loadVM(uuid, data, load_opts, function (e, obj) {
3717 if (e) {
3718 if (e.code === 'ENOENT') {
3719 // zone likely was deleted since lookup, ignore
3720 cb();
3721 } else {
3722 cb(e);
3723 }
3724 } else {
3725 if (transform) {
3726 transform(obj);
3727 }
3728 full_results.push(obj);
3729 cb();
3730 }
3731 });
3732 }
3733
3734 async.forEachSeries(results, expander, function (e) {
3735 var res_list;
3736
3737 if (e) {
3738 log.error(e, 'VM.lookup failed to expand results: '
3739 + e.message);
3740 callback(e);
3741 } else {
3742 res_list = full_results;
3743 if (options.hasOwnProperty('fields')) {
3744 filterFields(res_list);
3745 }
3746 callback(null,
3747 res_list.filter(function (res) {
3748 if (typeof (res) === 'object') {
3749 return (Object.keys(res).length > 0);
3750 } else {
3751 return (true);
3752 }
3753 })
3754 );
3755 }
3756 });
3757 } else {
3758 // have to search the hard way (through all the data)
3759 for (u in records) {
3760 uuids.push(u);
3761 }
3762 // this is parallel!
3763 async.forEach(uuids, function (uuid, cb) {
3764 var vmobj = records[uuid];
3765 var l_opts = {log: log};
3766
3767 if (options.hasOwnProperty('fields')
3768 && need_fields.length > 0) {
3769
3770 // we have a specific set of fields we want to grab
3771 l_opts.fields = need_fields;
3772 }
3773
3774 loadVM(vmobj.uuid, data, l_opts, function (error, obj) {
3775 if (error) {
3776 if (error.code === 'ENOENT') {
3777 // zone likely was deleted since lookup, ignore
3778 cb();
3779 } else {
3780 cb(error);
3781 }
3782 } else {
3783 if (transform) {
3784 transform(obj);
3785 }
3786 if (Object.keys(search).length === 0
3787 || matcher(obj, search)) {
3788
3789 results.push(obj);
3790 }
3791 cb();
3792 }
3793 });
3794 }, function (e) {
3795 var r;
3796 var short_results = [];
3797
3798 if (e) {
3799 callback(e);
3800 } else {
3801 if (options.full) {
3802 callback(null, results);
3803 } else if (options.fields && need_fields.length > 0) {
3804 if (options.hasOwnProperty('fields')) {
3805 filterFields(results);
3806 }
3807 callback(null,
3808 results.filter(function (res) {
3809 if (typeof (res) === 'object') {
3810 return (Object.keys(res).length > 0);
3811 } else {
3812 return (true);
3813 }
3814 })
3815 );
3816 } else {
3817 for (r in results) {
3818 short_results.push(results[r].uuid);
3819 }
3820 callback(null, short_results);
3821 }
3822 }
3823 });
3824 }
3825 });
3826 };
3827
3828 // create a random new locally administered MAC address
3829 function generateMAC()
3830 {
3831 var data = [(Math.floor(Math.random() * 15) + 1).toString(16) + 2];
3832 for (var i = 0; i < 5; i++) {
3833 var oct = (Math.floor(Math.random() * 255) + 1).toString(16);
3834 if (oct.length == 1) {
3835 oct = '0' + oct;
3836 }
3837 data.push(oct);
3838 }
3839
3840 return data.join(':');
3841 }
3842
3843 // return the MAC address based on a VRRP Virtual Router ID
3844 function vrrpMAC(vrid) {
3845 return sprintf('00:00:5e:00:01:%02x', vrid);
3846 }
3847
3848 // Ensure we've got all the datasets necessary to create this VM
3849 //
3850 // IMPORTANT:
3851 //
3852 // On SmartOS, we assume a provisioner or some other external entity has already
3853 // loaded the dataset into the system. This function just confirms that the
3854 // dataset actually exists.
3855 //
3856 function checkDatasets(payload, log, callback)
3857 {
3858 var checkme = [];
3859 var d;
3860 var disk;
3861
3862 assert(log, 'no logger passed to checkDatasets()');
3863
3864 log.debug('Checking for required datasets.');
3865
3866 // build list of datasets we need to download (downloadme)
3867 for (disk in payload.add_disks) {
3868 if (payload.add_disks.hasOwnProperty(disk)) {
3869 d = payload.add_disks[disk];
3870 if (d.hasOwnProperty('image_uuid')) {
3871 checkme.push(payload.zpool + '/'
3872 + d.image_uuid);
3873 }
3874 }
3875 }
3876
3877 function checker(dataset, cb) {
3878 zfs(['list', '-o', 'name', '-H', dataset], log, function (err, fds) {
3879 if (err) {
3880 log.error({'err': err, 'stdout': fds.stdout,
3881 'stderr': fds.stderr}, 'zfs list ' + dataset + ' '
3882 + 'exited with' + ' code ' + err.code + ': ' + err.message);
3883 cb(new Error('unable to find dataset: ' + dataset));
3884 } else {
3885 cb();
3886 }
3887 });
3888 }
3889
3890 // check that we have all the volumes
3891 async.forEachSeries(checkme, checker, function (err) {
3892 if (err) {
3893 log.error(err, 'checkDatasets() failed to find required '
3894 + 'volumes');
3895 callback(err);
3896 } else {
3897 // progress(100, 'we have all necessary datasets');
3898 callback();
3899 }
3900 });
3901 }
3902
3903 function lookupConflicts(macs, ips, vrids, log, callback) {
3904 var conflict = false;
3905 var load_fields;
3906
3907 load_fields = ['brand', 'state', 'nics', 'uuid', 'zonename', 'zone_state'];
3908
3909 assert(log, 'no logger passed to lookupConflicts()');
3910
3911 log.debug('checking for conflicts with '
3912 + JSON.stringify(macs) + ', ' + JSON.stringify(ips) + ' and '
3913 + JSON.stringify(vrids));
3914
3915 if (macs.length === 0 && ips.length === 0 && vrids.length === 0) {
3916 log.debug('returning from conflict check (nothing to check)');
3917 callback(null, conflict);
3918 return;
3919 }
3920
3921 preloadZoneData(null, {fields: load_fields, log: log},
3922 function (err, data) {
3923
3924 var records = data.records;
3925 var uuid;
3926 var uuids = [];
3927
3928 if (err) {
3929 callback(err);
3930 return;
3931 }
3932
3933 for (uuid in records) {
3934 uuids.push(uuid);
3935 }
3936
3937 // this is parallel!
3938 async.forEach(uuids, function (z_uuid, cb) {
3939 loadVM(z_uuid, data, {fields: load_fields, log: log},
3940 function (error, obj) {
3941
3942 var ip;
3943 var mac;
3944 var vrid;
3945
3946 if (error) {
3947 if (error.code === 'ENOENT') {
3948 // zone likely was deleted since lookup, ignore it
3949 cb();
3950 } else {
3951 cb(error);
3952 }
3953 return;
3954 }
3955
3956 if (obj.state === 'failed' && obj.zone_state !== 'running') {
3957 // Ignore zones that are failed unless they're 'running'
3958 // which they shouldn't be because they get stopped on
3959 // failure.
3960 cb();
3961 return;
3962 }
3963
3964 for (ip in ips) {
3965 if (ips[ip] !== 'dhcp'
3966 && matcher(obj, {'nics.*.ip': ips[ip]})) {
3967
3968 log.error('Found conflict: ' + obj.uuid
3969 + ' already has IP ' + ips[ip]);
3970 conflict = true;
3971 }
3972 }
3973 for (mac in macs) {
3974 if (matcher(obj, {'nics.*.mac': macs[mac]})) {
3975 log.error('Found conflict: ' + obj.uuid
3976 + ' already has MAC ' + macs[mac]);
3977 conflict = true;
3978 }
3979 }
3980 for (vrid in vrids) {
3981 if (matcher(obj, {'nics.*.vrrp_vrid': vrids[vrid]})) {
3982 log.error('Found conflict: ' + obj.uuid
3983 + ' already has VRID ' + vrids[vrid]);
3984 conflict = true;
3985 }
3986 }
3987 cb();
3988 });
3989 }, function (e) {
3990 if (e) {
3991 callback(e);
3992 } else {
3993 log.debug('returning from conflict check');
3994 callback(null, conflict);
3995 }
3996 });
3997 });
3998 }
3999
4000 function lookupInvalidNicTags(nics, log, callback) {
4001 var etherstubs = [];
4002 var nic_tags = {};
4003
4004 assert(log, 'no logger passed to lookupInvalidNicTags()');
4005
4006 if (!nics || nics.length === 0) {
4007 callback();
4008 return;
4009 }
4010
4011 async.parallel([
4012 function (cb) {
4013 dladm.showEtherstub(null, log, function (err, stubs) {
4014 if (err) {
4015 cb(err);
4016 } else {
4017 etherstubs = stubs;
4018 cb();
4019 }
4020 });
4021 }, function (cb) {
4022 VM.getSysinfo([], {log: log}, function (err, sysinfo) {
4023 if (err) {
4024 cb(err);
4025 } else {
4026 var nic;
4027 var tag;
4028 for (nic in sysinfo['Network Interfaces']) {
4029 nic = sysinfo['Network Interfaces'][nic];
4030 for (tag in nic['NIC Names']) {
4031 nic_tags[nic['NIC Names'][tag]] = 1;
4032 }
4033 }
4034 cb();
4035 }
4036 });
4037 }
4038 ], function (err, results) {
4039 if (err) {
4040 callback(err);
4041 return;
4042 }
4043
4044 var nic;
4045 for (nic in nics) {
4046 nic = nics[nic];
4047 if (!nic.hasOwnProperty('nic_tag')) {
4048 continue;
4049 }
4050 if (!nic_tags.hasOwnProperty(nic.nic_tag)
4051 && (etherstubs.indexOf(nic.nic_tag) === -1)) {
4052 callback(new Error('Invalid nic tag "' + nic.nic_tag + '"'));
4053 return;
4054 }
4055 }
4056
4057 callback();
4058 return;
4059 });
4060 }
4061
4062 // create a new zvol for a VM
4063 function createVolume(volume, log, callback)
4064 {
4065 var refreserv;
4066 var size;
4067 var snapshot;
4068
4069 assert(log, 'no logger passed for createVolume()');
4070
4071 log.debug('creating volume ' + JSON.stringify(volume));
4072
4073 if (volume.hasOwnProperty('image_size')) {
4074 size = volume.image_size;
4075 } else if (volume.hasOwnProperty('size')) {
4076 size = volume.size;
4077 } else {
4078 callback(new Error('FATAL: createVolume(' + JSON.stringify(volume)
4079 + '): ' + 'has no size or image_size'));
4080 return;
4081 }
4082
4083 if (volume.hasOwnProperty('refreservation')) {
4084 refreserv = volume.refreservation;
4085 } else {
4086 log.debug('defaulting to refreservation = ' + size);
4087 refreserv = size;
4088 }
4089
4090 async.series([
4091 function (cb) {
4092 if (volume.hasOwnProperty('image_uuid')) {
4093 snapshot = volume.zpool + '/' + volume.image_uuid + '@final';
4094 zfs(['get', '-Ho', 'value', 'name', snapshot], log,
4095 function (err, fds) {
4096
4097 if (err) {
4098 if (fds.stderr.match('dataset does not exist')) {
4099 // no @final, so we'll make a new snapshot @<uuid>
4100 snapshot = volume.zpool + '/' + volume.image_uuid
4101 + '@' + volume.uuid;
4102
4103 zfs(['snapshot', snapshot], log, function (e) {
4104 cb(e);
4105 });
4106 } else {
4107 cb(err);
4108 }
4109 } else {
4110 // @final is here!
4111 cb();
4112 }
4113 });
4114 } else {
4115 cb();
4116 }
4117 }, function (cb) {
4118 var args;
4119 var target;
4120
4121 target = volume.zpool + '/' + volume.uuid;
4122 if (volume.hasOwnProperty('image_uuid')) {
4123 // This volume is from a template/dataset/image so we create
4124 // it as a clone of a the @final snapshot on the original.
4125 // we already set 'snapshot' to the correct location above.
4126 args = ['clone', '-F'];
4127 if (volume.hasOwnProperty('compression')) {
4128 args.push('-o', 'compression='
4129 + volume.compression);
4130 }
4131 if (volume.hasOwnProperty('block_size')) {
4132 args.push('-o', 'volblocksize='
4133 + volume.block_size);
4134 }
4135 args.push('-o', 'refreservation=' + refreserv + 'M');
4136 args.push(snapshot, target);
4137 zfs(args, log, function (e) {
4138 if (e) {
4139 cb(e);
4140 } else {
4141 volume.path = '/dev/zvol/rdsk/' + target;
4142 cb();
4143 }
4144 });
4145 } else {
4146 // This volume is not from a template/dataset/image so we create
4147 // a blank new zvol for it.
4148 args = ['create'];
4149 if (volume.hasOwnProperty('compression')) {
4150 args.push('-o', 'compression='
4151 + volume.compression);
4152 }
4153 if (volume.hasOwnProperty('block_size')) {
4154 args.push('-o', 'volblocksize='
4155 + volume.block_size);
4156 }
4157 args.push('-o', 'refreservation=' + refreserv + 'M', '-V',
4158 size + 'M', target);
4159 zfs(args, log, function (err, fds) {
4160 if (err) {
4161 cb(err);
4162 } else {
4163 volume.path = '/dev/zvol/rdsk/' + target;
4164 cb();
4165 }
4166 });
4167 }
4168 }
4169 ], function (err, results) {
4170 callback(err);
4171 });
4172 }
4173
4174 // Create all the volumes for a given VM property set
4175 function createVolumes(payload, log, callback)
4176 {
4177 var createme = [];
4178 var d;
4179 var disk;
4180 var disk_idx = 0;
4181 var used_disk_indexes = [];
4182
4183 assert(log, 'no logger passed to createVolumes()');
4184
4185 log.debug('creating volumes: ' + JSON.stringify(payload.add_disks));
4186
4187 if (payload.hasOwnProperty('used_disk_indexes')) {
4188 used_disk_indexes = payload.used_disk_indexes;
4189 }
4190
4191 for (disk in payload.add_disks) {
4192 if (payload.add_disks.hasOwnProperty(disk)) {
4193 d = payload.add_disks[disk];
4194
4195 // we don't create CDROM devices or disk devices which have the
4196 // nocreate: true property.
4197 if (d.media !== 'cdrom' && !d.nocreate) {
4198 // skip to the next unused one.
4199 while (used_disk_indexes.indexOf(disk_idx) !== -1) {
4200 disk_idx++;
4201 }
4202
4203 d.index = disk_idx;
4204 d.uuid = payload.uuid + '-disk' + disk_idx;
4205 used_disk_indexes.push(Number(disk_idx));
4206 if (!d.hasOwnProperty('zpool')) {
4207 d.zpool = payload.zpool;
4208 }
4209 createme.push(d);
4210 }
4211 }
4212 }
4213
4214 function loggedCreateVolume(volume, cb) {
4215 return createVolume(volume, log, cb);
4216 }
4217
4218 // create all the volumes we found that we need.
4219 async.forEachSeries(createme, loggedCreateVolume, function (err) {
4220 if (err) {
4221 callback(err);
4222 } else {
4223 callback();
4224 }
4225 });
4226 }
4227
4228 // writes a Zone's metadata JSON to /zones/<uuid>/config/metadata.json
4229 // and /zones/<uuid>/config/tags.json.
4230 function updateMetadata(vmobj, payload, log, callback)
4231 {
4232 var cmdata = {};
4233 var imdata = {};
4234 var key;
4235 var mdata = {};
4236 var mdata_filename;
4237 var tags = {};
4238 var tags_filename;
4239 var zonepath;
4240
4241 assert(log, 'no logger passed to updateMetadata()');
4242
4243 if (vmobj.hasOwnProperty('zonepath')) {
4244 zonepath = vmobj.zonepath;
4245 } else if (vmobj.hasOwnProperty('zpool')
4246 && vmobj.hasOwnProperty('zonename')) {
4247
4248 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename;
4249 } else {
4250 callback(new Error('unable to find zonepath for '
4251 + JSON.stringify(vmobj)));
4252 return;
4253 }
4254
4255 // paths are under zonepath but not zoneroot
4256 mdata_filename = zonepath + '/config/metadata.json';
4257 tags_filename = zonepath + '/config/tags.json';
4258
4259 // customer_metadata
4260 for (key in vmobj.customer_metadata) {
4261 if (vmobj.customer_metadata.hasOwnProperty(key)) {
4262 cmdata[key] = vmobj.customer_metadata[key];
4263 if (payload.hasOwnProperty('remove_customer_metadata')
4264 && payload.remove_customer_metadata.indexOf(key) !== -1) {
4265
4266 // in the remove_* list, don't load it.
4267 delete cmdata[key];
4268 }
4269 }
4270 }
4271
4272 for (key in payload.set_customer_metadata) {
4273 if (payload.set_customer_metadata.hasOwnProperty(key)) {
4274 cmdata[key] = payload.set_customer_metadata[key];
4275 }
4276 }
4277
4278 // internal_metadata
4279 for (key in vmobj.internal_metadata) {
4280 if (vmobj.internal_metadata.hasOwnProperty(key)) {
4281 imdata[key] = vmobj.internal_metadata[key];
4282 if (payload.hasOwnProperty('remove_internal_metadata')
4283 && payload.remove_internal_metadata.indexOf(key) !== -1) {
4284
4285 // in the remove_* list, don't load it.
4286 delete imdata[key];
4287 }
4288 }
4289 }
4290
4291 for (key in payload.set_internal_metadata) {
4292 if (payload.set_internal_metadata.hasOwnProperty(key)) {
4293 imdata[key] = payload.set_internal_metadata[key];
4294 }
4295 }
4296
4297 // same thing for tags
4298 for (key in vmobj.tags) {
4299 if (vmobj.tags.hasOwnProperty(key)) {
4300 tags[key] = vmobj.tags[key];
4301 if (payload.hasOwnProperty('remove_tags')
4302 && payload.remove_tags.indexOf(key) !== -1) {
4303
4304 // in the remove_* list, don't load it.
4305 delete tags[key];
4306 }
4307 }
4308 }
4309
4310 for (key in payload.set_tags) {
4311 if (payload.set_tags.hasOwnProperty(key)) {
4312 tags[key] = payload.set_tags[key];
4313 }
4314 }
4315
4316 mdata = {'customer_metadata': cmdata, 'internal_metadata': imdata};
4317 fs.writeFile(mdata_filename, JSON.stringify(mdata, null, 2),
4318 function (err) {
4319 if (err) {
4320 callback(err);
4321 } else {
4322 log.debug('wrote metadata to ' + mdata_filename);
4323 fs.writeFile(tags_filename, JSON.stringify(tags, null, 2),
4324 function (e) {
4325 if (e) {
4326 callback(e);
4327 } else {
4328 log.debug('wrote tags to' + tags_filename);
4329 callback();
4330 }
4331 }
4332 );
4333 }
4334 }
4335 );
4336 }
4337
4338 function saveMetadata(payload, log, callback)
4339 {
4340 var protovm = {};
4341
4342 assert(log, 'no logger passed to saveMetadata()');
4343
4344 if (!payload.hasOwnProperty('zonepath')
4345 || !payload.hasOwnProperty('zpool')
4346 || !payload.hasOwnProperty('zonename')) {
4347
4348 callback(new Error('saveMetadata payload is missing zone '
4349 + 'properties.'));
4350 return;
4351 }
4352
4353 protovm.zonepath = payload.zonepath;
4354 protovm.zpool = payload.zpool;
4355 protovm.zonename = payload.zonename;
4356 protovm.customer_metadata = {};
4357 protovm.tags = {};
4358
4359 if (payload.hasOwnProperty('tags')) {
4360 payload.set_tags = payload.tags;
4361 delete payload.tags;
4362 }
4363 if (payload.hasOwnProperty('customer_metadata')) {
4364 payload.set_customer_metadata = payload.customer_metadata;
4365 delete payload.customer_metadata;
4366 }
4367 if (payload.hasOwnProperty('internal_metadata')) {
4368 payload.set_internal_metadata = payload.internal_metadata;
4369 delete payload.internal_metadata;
4370 }
4371
4372 updateMetadata(protovm, payload, log, callback);
4373 }
4374
4375 // writes a zone's metadata JSON to /zones/<uuid>/config/routes.json
4376 function updateRoutes(vmobj, payload, log, callback)
4377 {
4378 var filename;
4379 var key;
4380 var routes = {};
4381 var zonepath;
4382
4383 assert(log, 'no logger passed to updateRoutes()');
4384
4385 if (vmobj.hasOwnProperty('zonepath')) {
4386 zonepath = vmobj.zonepath;
4387 } else if (vmobj.hasOwnProperty('zpool')
4388 && vmobj.hasOwnProperty('zonename')) {
4389
4390 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename;
4391 } else {
4392 callback(new Error('unable to find zonepath for '
4393 + JSON.stringify(vmobj)));
4394 return;
4395 }
4396
4397 // paths are under zonepath but not zoneroot
4398 filename = zonepath + '/config/routes.json';
4399
4400 for (key in vmobj.routes) {
4401 if (vmobj.routes.hasOwnProperty(key)) {
4402 routes[key] = vmobj.routes[key];
4403 if (payload.hasOwnProperty('remove_routes')
4404 && payload.remove_routes.indexOf(key) !== -1) {
4405
4406 // in the remove_* list, don't load it.
4407 delete routes[key];
4408 }
4409 }
4410 }
4411
4412 for (key in payload.set_routes) {
4413 if (payload.set_routes.hasOwnProperty(key)) {
4414 routes[key] = payload.set_routes[key];
4415 }
4416 }
4417
4418 fs.writeFile(filename, JSON.stringify(routes, null, 2),
4419 function (err) {
4420 if (err) {
4421 callback(err);
4422 } else {
4423 log.debug('wrote routes to ' + filename);
4424 callback();
4425 }
4426 });
4427 }
4428
4429 function saveRoutes(payload, log, callback)
4430 {
4431 var protovm = {};
4432
4433 assert(log, 'no logger passed to saveRoutes()');
4434
4435 if (!payload.hasOwnProperty('zonepath')
4436 || !payload.hasOwnProperty('zpool')
4437 || !payload.hasOwnProperty('zonename')) {
4438
4439 callback(new Error('saveRoutes payload is missing zone '
4440 + 'properties.'));
4441 return;
4442 }
4443
4444 protovm.zonepath = payload.zonepath;
4445 protovm.zpool = payload.zpool;
4446 protovm.zonename = payload.zonename;
4447
4448 if (payload.hasOwnProperty('routes')) {
4449 payload.set_routes = payload.routes;
4450 delete payload.routes;
4451 }
4452
4453 updateRoutes(protovm, payload, log, callback);
4454 }
4455
4456 function createVM(payload, log, callback)
4457 {
4458 assert(log, 'no logger passed to createVM()');
4459
4460 async.series([
4461 function (cb) {
4462 if (!payload.create_only) {
4463 // progress(2, 'checking required datasets');
4464 checkDatasets(payload, log, cb);
4465 } else {
4466 cb();
4467 }
4468 }, function (cb) {
4469 if (!payload.create_only) {
4470 // progress(29, 'creating volumes');
4471 createVolumes(payload, log, cb);
4472 } else {
4473 cb();
4474 }
4475 }, function (cb) {
4476 // progress(51, 'creating zone container');
4477 createZone(payload, log, cb);
4478 }
4479 ], function (err, results) {
4480 if (err) {
4481 callback(err);
4482 } else {
4483 callback(null, results);
4484 }
4485 });
4486 }
4487
4488 function fixZoneinitMetadataSock(zoneroot, log, callback)
4489 {
4490 var mdata_00;
4491
4492 // ensure we're safe to touch these files, zone should not be running here
4493 // so this just guards against malicious datasets.
4494 ['/var/zoneinit/includes', '/root/zoneinit.d'].forEach(function (dir) {
4495 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true});
4496 });
4497
4498 function replaceData(filename, cb) {
4499 fs.readFile(filename, 'utf8', function (error, data) {
4500 if (error) {
4501 log.error(error, 'failed to load 00-mdata.sh for replacement');
4502 cb(error);
4503 return;
4504 }
4505
4506 data = data.replace(/\/var\/run\/smartdc\/metadata.sock/g,
4507 '/.zonecontrol/metadata.sock');
4508
4509 log.trace('writing [' + data + '] to ' + filename);
4510 fs.writeFile(filename, data, 'utf8', function (err) {
4511 if (err) {
4512 log.error(err, 'failed to write ' + filename);
4513 }
4514 cb(err);
4515 });
4516 });
4517 }
4518
4519 // try /var/zoneinit/includes/00-mdata.sh first, since that's in new images
4520 mdata_00 = path.join(zoneroot, '/var/zoneinit/includes/00-mdata.sh');
4521 fs.exists(mdata_00, function (exists1) {
4522 if (exists1) {
4523 log.info('fixing socket in /var/zoneinit/includes/00-mdata.sh');
4524 replaceData(mdata_00, callback);
4525 } else {
4526 // didn't exist, so try location it exists in older images eg. 1.6.3
4527 mdata_00 = path.join(zoneroot, '/root/zoneinit.d/00-mdata.sh');
4528 fs.exists(mdata_00, function (exists2) {
4529 if (exists2) {
4530 log.info('fixing socket in /root/zoneinit.d/00-mdata.sh');
4531 replaceData(mdata_00, callback);
4532 } else {
4533 log.info('no 00-mdata.sh to cleanup in zoneinit');
4534 callback();
4535 }
4536 });
4537 }
4538 });
4539 }
4540
4541 function fixMdataFetchStart(zonepath, log, callback)
4542 {
4543 // svccfg validates zonepath
4544 var mdata_fetch_start = '/lib/svc/method/mdata-fetch';
4545
4546 svccfg(zonepath, ['-s', 'svc:/smartdc/mdata:fetch', 'setprop', 'start/exec',
4547 '=', mdata_fetch_start], log, function (error, stdio) {
4548
4549 if (error) {
4550 log.error(error, 'failed to set mdata:fetch start method');
4551 } else {
4552 log.info('successfully set mdata:fetch start method');
4553 }
4554
4555 callback(error);
4556 });
4557 }
4558
4559 function cleanupMessyDataset(zonepath, brand, log, callback)
4560 {
4561 var command;
4562 var zoneroot = path.join(zonepath, '/root');
4563
4564 assert(log, 'no logger passed to cleanupMessyDataset()');
4565
4566 try {
4567 ['/var/adm', '/var/svc/log', '/var/svc/manifest', '/root/zoneinit.d']
4568 .forEach(function (dir) {
4569
4570 // This will ensure these are safe if they exist.
4571 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true});
4572 });
4573 } catch (e) {
4574 log.error(e, 'Unable to cleanup dataset: ' + e.message);
4575 callback(e);
4576 return;
4577 }
4578
4579 // We've verified the directories here exist, and have no symlinks in the
4580 // path (or don't exist) so rm -f <dir>/<file> should be safe regardless of
4581 // the type of <file>
4582
4583 command = 'rm -f '
4584 + zoneroot + '/var/adm/utmpx '
4585 + zoneroot + '/var/adm/wtmpx '
4586 + zoneroot + '/var/svc/log/*.log '
4587 + zoneroot + '/var/svc/mdata '
4588 + zoneroot + '/var/svc/manifest/mdata.xml ';
4589
4590 if (! BRAND_OPTIONS[brand].features.zoneinit) {
4591 // eg. joyent-minimal (don't need zoneinit)
4592 command = command + zoneroot + '/root/zoneinit.xml '
4593 + zoneroot + '/root/zoneinit '
4594 + '&& rm -rf ' + zoneroot + '/root/zoneinit.d ';
4595 }
4596
4597 command = command + '&& touch ' + zoneroot + '/var/adm/wtmpx';
4598 log.debug(command);
4599 exec(command, function (error, stdout, stderr) {
4600 log.debug({err: error, stdout: stdout, stderr: stderr},
4601 'returned from cleaning up dataset');
4602 if (error || !BRAND_OPTIONS[brand].features.zoneinit) {
4603 // either we already failed or this zone doesn't use zoneinit so
4604 // we don't need to bother fixing zoneinit's scripts.
4605 callback(error);
4606 } else {
4607 fixZoneinitMetadataSock(zoneroot, log, function (err) {
4608 // See OS-2314, currently we assume all zones w/ zoneinit also
4609 // have broken mdata:fetch when images are created from them.
4610 // Attempt to fix that too.
4611 fixMdataFetchStart(zonepath, log, callback);
4612 });
4613 }
4614 });
4615 }
4616
4617 // Helper for unlinking and replacing a file that you've already confirmed
4618 // has no symlinks. Throws error when fs.writeFileSync does, or when
4619 // fs.unlinkSync throws non ENOENT.
4620 function replaceFile(zoneroot, filename, data) {
4621 // first delete, in case file itself is a link
4622 try {
4623 fs.unlinkSync(path.join(zoneroot, filename));
4624 } catch (e) {
4625 if (e.code !== 'ENOENT') {
4626 throw e;
4627 }
4628 }
4629
4630 fs.writeFileSync(path.join(zoneroot, filename), data);
4631 }
4632
4633 // NOTE: we write these out initially before the zone is started, but after that
4634 // rely on mdata-fetch in the zone to do the updates since we can't safely write
4635 // these files in the zones.
4636 function writeZoneNetfiles(payload, log, callback)
4637 {
4638 var hostname;
4639 var n;
4640 var nic;
4641 var primary_found = false;
4642 var zoneroot;
4643
4644 assert(log, 'no logger passed to writeZoneNetfiles()');
4645 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload');
4646
4647 zoneroot = payload.zonepath + '/root';
4648
4649 try {
4650 assertSafeZonePath(zoneroot, '/etc', {type: 'dir', enoent_ok: true});
4651 } catch (e) {
4652 log.error(e, 'Unable to write zone net files: ' + e.message);
4653 callback(e);
4654 return;
4655 }
4656
4657 log.info('Writing network files to zone root');
4658
4659 try {
4660 for (nic in payload.add_nics) {
4661 if (payload.add_nics.hasOwnProperty(nic)) {
4662 n = payload.add_nics[nic];
4663
4664 if (n.ip != 'dhcp') {
4665 replaceFile(zoneroot, '/etc/hostname.'
4666 + n.interface, n.ip + ' netmask ' + n.netmask
4667 + ' up' + '\n');
4668 }
4669
4670 if (n.hasOwnProperty('primary') && !primary_found) {
4671 // only allow one primary network
4672 primary_found = true;
4673 if (n.hasOwnProperty('gateway')) {
4674 replaceFile(zoneroot, '/etc/defaultrouter',
4675 n.gateway + '\n');
4676 }
4677 if (n.ip == 'dhcp') {
4678 replaceFile(zoneroot, '/etc/dhcp.' + n.interface, '');
4679 }
4680 }
4681 }
4682 }
4683
4684 // It's possible we don't have zonename or hostname set because of the
4685 // ordering of adding the UUID. In any case, we'll have at least a uuid
4686 // here.
4687 if (payload.hasOwnProperty('hostname')) {
4688 hostname = payload.hostname;
4689 } else if (payload.hasOwnProperty('zonename')) {
4690 hostname = payload.zonename;
4691 } else {
4692 hostname = payload.uuid;
4693 }
4694
4695 replaceFile(zoneroot, '/etc/nodename', hostname + '\n');
4696 } catch (e) {
4697 log.error(e, 'Unable to write zone networking files: ' + e.message);
4698 callback(e);
4699 return;
4700 }
4701
4702 callback();
4703 }
4704
4705 /*
4706 * NOTE: once we no longer support old datasets that need the 'zoneconfig' file,
4707 * this function and calls to it can be removed.
4708 *
4709 * This writes out the zoneconfig file that is used by the zoneinit service in
4710 * joyent branded zones' datasets.
4711 *
4712 */
4713 function writeZoneconfig(payload, log, callback)
4714 {
4715 var data;
4716 var hostname;
4717 var n;
4718 var nic;
4719 var zoneroot;
4720
4721 assert(log, 'no logger passed to writeZoneconfig()');
4722 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload');
4723
4724 zoneroot = payload.zonepath + '/root';
4725
4726 log.info('Writing config for zoneinit');
4727
4728 if (payload.hasOwnProperty('hostname')) {
4729 hostname = payload.hostname;
4730 } else {
4731 hostname = payload.zonename;
4732 }
4733
4734 data = 'TEMPLATE_VERSION=0.0.1\n'
4735 + 'ZONENAME=' + payload.zonename + '\n'
4736 + 'HOSTNAME=' + hostname + '.' + payload.dns_domain + '\n'
4737 + 'TMPFS=' + payload.tmpfs + 'm\n';
4738
4739 if (payload.hasOwnProperty('add_nics') && payload.add_nics[0]) {
4740
4741 if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') {
4742 data = data + 'PUBLIC_IP=' + payload.add_nics[0].ip + '\n';
4743 }
4744 if (payload.add_nics[1] && payload.add_nics[1].ip != 'dhcp') {
4745 data = data + 'PRIVATE_IP=' + payload.add_nics[1].ip + '\n';
4746 } else if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') {
4747 // zoneinit uses private_ip for /etc/hosts, we want to
4748 // make that same as public, if there's no actual private.
4749 data = data + 'PRIVATE_IP=' + payload.add_nics[0].ip + '\n';
4750 }
4751 }
4752
4753 if (payload.hasOwnProperty('resolvers')) {
4754 // zoneinit appends to resolv.conf rather than overwriting, so just
4755 // add to the zoneconfig and let zoneinit handle it
4756 data = data + 'RESOLVERS="' + payload.resolvers.join(' ') + '"\n';
4757 }
4758
4759 for (nic in payload.add_nics) {
4760 if (payload.add_nics.hasOwnProperty(nic)) {
4761 n = payload.add_nics[nic];
4762 data = data + n.interface.toUpperCase() + '_MAC=' + n.mac + '\n'
4763 + n.interface.toUpperCase() + '_INTERFACE='
4764 + n.interface.toUpperCase() + '\n';
4765
4766 if (n.ip != 'dhcp') {
4767 data = data + n.interface.toUpperCase() + '_IP=' + n.ip + '\n'
4768 + n.interface.toUpperCase() + '_NETMASK='
4769 + n.netmask + '\n';
4770 }
4771 }
4772 }
4773
4774 try {
4775 assertSafeZonePath(zoneroot, '/var/svc/log/system-zoneinit:default.log',
4776 {type: 'file', enoent_ok: true});
4777 assertSafeZonePath(zoneroot, '/root/zoneconfig',
4778 {type: 'file', enoent_ok: true});
4779
4780 replaceFile(zoneroot, '/var/svc/log/system-zoneinit:default.log', '');
4781
4782 log.debug('writing zoneconfig ' + JSON.stringify(data) + ' to '
4783 + zoneroot);
4784
4785 replaceFile(zoneroot, '/root/zoneconfig', data);
4786 callback();
4787 } catch (e) {
4788 log.error(e, 'Unable to write zoneconfig files: ' + e.message);
4789 callback(e);
4790 return;
4791 }
4792 }
4793
4794 function zonecfg(args, log, callback)
4795 {
4796 var cmd = '/usr/sbin/zonecfg';
4797
4798 assert(log, 'no logger passed to zonecfg()');
4799
4800 log.debug(cmd + ' ' + args.join(' '));
4801 execFile(cmd, args, function (error, stdout, stderr) {
4802 if (error) {
4803 callback(error, {'stdout': stdout, 'stderr': stderr});
4804 } else {
4805 callback(null, {'stdout': stdout, 'stderr': stderr});
4806 }
4807 });
4808 }
4809
4810 function zonecfgFile(data, args, log, callback)
4811 {
4812 var tmpfile = '/tmp/zonecfg.' + process.pid + '.tmp';
4813
4814 assert(log, 'no logger passed to zonecfgFile()');
4815
4816 fs.writeFile(tmpfile, data, function (err, result) {
4817 if (err) {
4818 // On failure we don't delete the tmpfile so we can debug it.
4819 callback(err);
4820 } else {
4821 args.push('-f');
4822 args.push(tmpfile);
4823
4824 zonecfg(args, log, function (e, fds) {
4825 if (e) {
4826 // keep temp file around for investigation
4827 callback(e, fds);
4828 } else {
4829 fs.unlink(tmpfile, function () {
4830 callback(null, fds);
4831 });
4832 }
4833 });
4834 }
4835 });
4836 }
4837
4838 function zoneadm(args, log, callback)
4839 {
4840 var cmd = '/usr/sbin/zoneadm';
4841
4842 assert(log, 'no logger passed to zoneadm()');
4843
4844 log.debug(cmd + ' ' + args.join(' '));
4845 execFile(cmd, args, function (error, stdout, stderr) {
4846 if (error) {
4847 callback(error, {'stdout': stdout, 'stderr': stderr});
4848 } else {
4849 callback(null, {'stdout': stdout, 'stderr': stderr});
4850 }
4851 });
4852 }
4853
4854 function zfs(args, log, callback)
4855 {
4856 var cmd = '/usr/sbin/zfs';
4857
4858 assert(log, 'no logger passed to zfs()');
4859
4860 log.debug(cmd + ' ' + args.join(' '));
4861 execFile(cmd, args, function (error, stdout, stderr) {
4862 if (error) {
4863 callback(error, {'stdout': stdout, 'stderr': stderr});
4864 } else {
4865 callback(null, {'stdout': stdout, 'stderr': stderr});
4866 }
4867 });
4868 }
4869
4870 exports.getSysinfo = function (args, options, callback)
4871 {
4872 var cmd = '/usr/bin/sysinfo';
4873 var log;
4874
4875 // we used to allow just one argument (callback) and we also allow 2 args
4876 // (args, callback) so that options is optional.
4877 if (arguments.length === 1) {
4878 callback = arguments[0];
4879 args = [];
4880 options = {};
4881 }
4882 if (arguments.length === 2) {
4883 callback = arguments[1];
4884 options = {};
4885 }
4886
4887 ensureLogging(false);
4888 if (options.hasOwnProperty('log')) {
4889 log = options.log;
4890 } else {
4891 log = VM.log.child({action: 'getSysinfo'});
4892 }
4893
4894 log.debug(cmd + ' ' + args.join(' '));
4895 execFile(cmd, args, function (error, stdout, stderr) {
4896 var sysinfo;
4897
4898 if (error) {
4899 callback(error, {'stdout': stdout, 'stderr': stderr});
4900 } else {
4901 try {
4902 sysinfo = JSON.parse(stdout.toString());
4903 } catch (e) {
4904 sysinfo = {};
4905 }
4906 callback(null, sysinfo);
4907 }
4908 });
4909 };
4910
4911 /*
4912 * This watches zone transitions and calls callback when specified
4913 * state is reached. Optionally you can set a timeout which will
4914 * call your callback when the timeout occurs whether the transition
4915 * has happened or not.
4916 *
4917 * payload needs to have at least .zonename and .uuid
4918 *
4919 */
4920 exports.waitForZoneState = function (payload, state, options, callback)
4921 {
4922 var log;
4923 var sysevent_state;
4924 var timeout;
4925 var timeout_secs = PROVISION_TIMEOUT;
4926 var watcher;
4927
4928 // options is optional
4929 if (arguments.length === 3) {
4930 callback = arguments[2];
4931 options = {};
4932 }
4933
4934 ensureLogging(false);
4935 if (options.hasOwnProperty('log')) {
4936 log = options.log;
4937 } else {
4938 log = VM.log.child({action: 'waitForZoneState', vm: payload.uuid});
4939 }
4940
4941 if (options.hasOwnProperty('timeout')) {
4942 timeout_secs = options.timeout;
4943 }
4944
4945 sysevent_state = state;
4946 if (state === 'installed') {
4947 // Apparently the zone status 'installed' equals sysevent status
4948 // 'uninitialized'
4949 sysevent_state = 'uninitialized';
4950 }
4951
4952 function done() {
4953 if (timeout) {
4954 clearTimeout(timeout);
4955 timeout = null;
4956 }
4957 }
4958
4959 function handler(err, obj) {
4960 if (err) {
4961 done();
4962 callback(err);
4963 return;
4964 }
4965 log.trace('handler got: ' + JSON.stringify(obj));
4966 if (obj.zonename !== payload.zonename) {
4967 return;
4968 }
4969
4970 if (obj.newstate === sysevent_state) {
4971 // Load again to confirm
4972 VM.lookup({'zonename': obj.zonename},
4973 {fields: ['zone_state'], log: log},
4974 function (error, res) {
4975 var handler_retry;
4976
4977 if (error) {
4978 watcher.cleanup();
4979 done();
4980 callback(error);
4981 return;
4982 }
4983
4984 if (res.length !== 1) {
4985 watcher.cleanup();
4986 done();
4987 callback(new Error('lookup could no find VM '
4988 + obj.zonename));
4989 return;
4990 }
4991
4992 if (res[0].hasOwnProperty('zone_state')
4993 && res[0].zone_state === state) {
4994
4995 // found the state we're looking for, success!
4996 log.debug('saw zone go to ' + obj.newstate + ' ('
4997 + state + ') calling callback()');
4998 watcher.cleanup();
4999 done();
5000 callback();
5001 } else if (timeout) {
5002 // we saw a state change to a state we don't care about
5003 // so if we've not timed out try reloading again in a
5004 // second.
5005 if (!handler_retry) {
5006 handler_retry = setTimeout(function () {
5007 if (timeout) {
5008 // try again if wait timeout is still set
5009 handler(null, obj);
5010 }
5011 handler_retry = null;
5012 }, 1000);
5013 log.debug('zone state after lookup: '
5014 + res[0].zone_state + ', still waiting');
5015 } else {
5016 log.debug('zone in wrong state but we already'
5017 + ' have a handler running');
5018 }
5019 } else {
5020 // no timeout set and we're not at the correct state
5021 log.error('failed to reach state: ' + state);
5022 callback(new Error('failed to reach state: ' + state));
5023 }
5024 }
5025 );
5026 }
5027 }
5028
5029 watcher = watchZoneTransitions(handler, log);
5030
5031 timeout = setTimeout(function () {
5032 var err;
5033
5034 done();
5035 watcher.cleanup();
5036 err = new Error('timed out waiting for zone to transition to ' + state);
5037 err.code = 'ETIMEOUT';
5038 callback(err);
5039 }, timeout_secs * 1000);
5040
5041 // after we've started the watcher (if we checked before there'd be a race)
5042 // we check whether we're already in the target state, if we are close it
5043 // down and return.
5044 VM.load(payload.uuid, {fields: ['zone_state'], log: log},
5045 function (err, obj) {
5046
5047 if (err) {
5048 watcher.cleanup();
5049 done();
5050 callback(err);
5051 } else if (obj.hasOwnProperty('zone_state')
5052 && obj.zone_state === state) {
5053
5054 watcher.cleanup();
5055 done();
5056 log.info('VM is in state ' + state);
5057 callback(); // at correct state!
5058 }
5059 });
5060 };
5061
5062 // handler() will be called with an object describing the transition for any
5063 // transitions seen (after any filtering). The only filtering here is to remove
5064 // duplicate events. Other filtering should be done by the caller.
5065 function watchZoneTransitions(handler, log) {
5066 var buffer = '';
5067 var chunks;
5068 var cleanup;
5069 var watcher;
5070 var watcher_pid;
5071
5072 assert(log, 'no logger passed to watchZoneTransitions()');
5073
5074 if (!zoneevent) {
5075
5076 zoneevent = new EventEmitter();
5077
5078 log.debug('/usr/vm/sbin/zoneevent');
5079 watcher = spawn('/usr/vm/sbin/zoneevent', [],
5080 {'customFds': [-1, -1, -1]});
5081 log.debug('zoneevent running with pid ' + watcher.pid);
5082 watcher_pid = watcher.pid;
5083
5084 watcher.stdout.on('data', function (data) {
5085 var chunk;
5086 var obj;
5087 var prev_msg;
5088
5089 buffer += data.toString();
5090 chunks = buffer.split('\n');
5091 while (chunks.length > 1) {
5092 chunk = chunks.shift();
5093 obj = JSON.parse(chunk);
5094
5095 if (obj === prev_msg) {
5096 // Note: sometimes sysevent emits multiple events for the
5097 // same status, we only want the first one here because just
5098 // because sysevent does it, doesn't make it right.
5099 log.debug('duplicate zoneevent message! '
5100 + JSON.stringify(obj));
5101 } else if (zoneevent) {
5102 zoneevent.emit('zoneevent', null, obj);
5103 }
5104 }
5105 buffer = chunks.pop();
5106 });
5107
5108 // doesn't take input.
5109 watcher.stdin.end();
5110
5111 watcher.on('exit', function (code) {
5112 log.warn('zoneevent watcher ' + watcher_pid + ' exited: ',
5113 JSON.stringify(code));
5114 // tell all the listeners of this zoneevent (if there are any) that
5115 // we exited. Then null it out so next time we'll make a new one.
5116 zoneevent.emit('zoneevent', new Error('zoneevent watcher exited '
5117 + 'prematurely with code: ' + code));
5118 zoneevent = null;
5119 });
5120 }
5121
5122 cleanup = function () {
5123 var listeners;
5124
5125 if (zoneevent) {
5126 listeners = zoneevent.listeners('zoneevent');
5127
5128 log.debug('cleanup called w/ listeners: '
5129 + util.inspect(listeners));
5130 zoneevent.removeListener('zoneevent', handler);
5131 if (zoneevent.listeners('zoneevent').length === 0) {
5132 log.debug('zoneevent watcher ' + watcher_pid
5133 + ' cleanup called');
5134 zoneevent = null;
5135 if (watcher) {
5136 watcher.stdout.destroy(); // so we don't send more 'data'
5137 watcher.stderr.destroy();
5138 watcher.removeAllListeners('exit'); // so don't fail on kill
5139 watcher.kill();
5140 watcher = null;
5141 }
5142 }
5143 } else if (watcher) {
5144 watcher.stdout.destroy(); // so we don't send more 'data'
5145 watcher.stderr.destroy();
5146 watcher.removeAllListeners('exit'); // so don't fail on our kill
5147 watcher.kill();
5148 watcher = null;
5149 }
5150 };
5151
5152 zoneevent.on('zoneevent', handler);
5153
5154 return ({'cleanup': cleanup});
5155 }
5156
5157 function fixPayloadMemory(payload, vmobj, log)
5158 {
5159 var brand;
5160 var max_locked;
5161 var max_phys;
5162 var min_overhead;
5163 var ram;
5164
5165 assert(log, 'no logger passed to fixPayloadMemory()');
5166
5167 if (vmobj.hasOwnProperty('brand')) {
5168 brand = vmobj.brand;
5169 } else if (payload.hasOwnProperty('brand')) {
5170 brand = payload.brand;
5171 }
5172
5173 if (BRAND_OPTIONS[brand].features.default_memory_overhead
5174 && payload.hasOwnProperty('ram')
5175 && !payload.hasOwnProperty('max_physical_memory')) {
5176
5177 // For now we add overhead to the memory caps for KVM zones, this
5178 // is for the qemu process itself. Since customers don't have direct
5179 // access to zone memory, this exists mostly to protect against bugs.
5180 payload.max_physical_memory = (payload.ram
5181 + BRAND_OPTIONS[brand].features.default_memory_overhead);
5182 } else if (payload.hasOwnProperty('ram')
5183 && !payload.hasOwnProperty('max_physical_memory')) {
5184
5185 payload.max_physical_memory = payload.ram;
5186 }
5187
5188 if (payload.hasOwnProperty('max_physical_memory')) {
5189 if (!payload.hasOwnProperty('max_locked_memory')) {
5190 if (vmobj.hasOwnProperty('max_locked_memory')
5191 && vmobj.hasOwnProperty('max_physical_memory')) {
5192
5193 // we don't have a new value, so first try to keep the same
5194 // delta that existed before btw. max_phys and max_locked
5195 payload.max_locked_memory = payload.max_physical_memory
5196 - (vmobj.max_physical_memory - vmobj.max_locked_memory);
5197 } else {
5198 // existing obj doesn't have max_locked, add one now
5199 payload.max_locked_memory = payload.max_physical_memory;
5200 }
5201 }
5202
5203 if (!payload.hasOwnProperty('max_swap')) {
5204 if (vmobj.hasOwnProperty('max_swap')
5205 && vmobj.hasOwnProperty('max_physical_memory')) {
5206
5207 // we don't have a new value, so first try to keep the same
5208 // delta that existed before btw. max_phys and max_swap
5209 if (vmobj.max_swap === MINIMUM_MAX_SWAP
5210 && vmobj.max_swap <= MINIMUM_MAX_SWAP
5211 && payload.max_physical_memory >= MINIMUM_MAX_SWAP) {
5212 // in this case we artificially inflated before to meet
5213 // minimum tie back to ram.
5214 payload.max_swap = payload.max_physical_memory;
5215 } else {
5216 payload.max_swap = payload.max_physical_memory
5217 + (vmobj.max_swap - vmobj.max_physical_memory);
5218 }
5219 } else {
5220 // existing obj doesn't have max_swap, add one now
5221 payload.max_swap = payload.max_physical_memory;
5222 }
5223
5224 // never add a max_swap less than MINIMUM_MAX_SWAP
5225 if (payload.max_swap < MINIMUM_MAX_SWAP) {
5226 payload.max_swap = MINIMUM_MAX_SWAP;
5227 }
5228 }
5229 }
5230
5231 // if we're updating tmpfs it must be lower than our new max_physical or
5232 // if we're not also changing max_physical, it must be lower than the
5233 // current one.
5234 if (payload.hasOwnProperty('tmpfs')) {
5235 if (payload.hasOwnProperty('max_physical_memory')
5236 && (Number(payload.tmpfs)
5237 > Number(payload.max_physical_memory))) {
5238
5239 payload.tmpfs = payload.max_physical_memory;
5240 } else if (Number(payload.tmpfs)
5241 > Number(vmobj.max_physical_memory)) {
5242
5243 payload.tmpfs = vmobj.max_physical_memory;
5244 }
5245 }
5246
5247 if (payload.hasOwnProperty('max_physical_memory')
5248 && BRAND_OPTIONS[brand].features.use_tmpfs
5249 && !payload.hasOwnProperty('tmpfs')) {
5250
5251 if (vmobj.hasOwnProperty('max_physical_memory')
5252 && vmobj.hasOwnProperty('tmpfs')) {
5253
5254 // change tmpfs to be the same ratio of ram as before
5255 payload.tmpfs = ((vmobj.tmpfs / vmobj.max_physical_memory)
5256 * payload.max_physical_memory);
5257 payload.tmpfs = Number(payload.tmpfs).toFixed();
5258 } else {
5259 // tmpfs must be < max_physical_memory, if not: pretend it was
5260 payload.tmpfs = payload.max_physical_memory;
5261 }
5262 }
5263
5264 // now that we've possibly adjusted target values, lower/raise values to
5265 // satisify max/min.
5266
5267 min_overhead = BRAND_OPTIONS[brand].features.min_memory_overhead;
5268 if (min_overhead) {
5269 ram = payload.hasOwnProperty('ram') ? payload.ram : vmobj.ram;
5270 max_phys = payload.hasOwnProperty('max_physical_memory')
5271 ? payload.max_physical_memory : vmobj.max_physical_memory;
5272 max_locked = payload.hasOwnProperty('max_locked_memory')
5273 ? payload.max_locked_memory : vmobj.max_locked_memory;
5274
5275 if ((ram + min_overhead) > max_phys) {
5276 payload.max_physical_memory = (ram + min_overhead);
5277 }
5278 if ((ram + min_overhead) > max_locked) {
5279 payload.max_locked_memory = (ram + min_overhead);
5280 }
5281 }
5282
5283 if (payload.hasOwnProperty('max_locked_memory')) {
5284 if (payload.hasOwnProperty('max_physical_memory')) {
5285 if (payload.max_locked_memory > payload.max_physical_memory) {
5286 log.warn('max_locked_memory (' + payload.max_locked_memory
5287 + ') > max_physical_memory (' + payload.max_physical_memory
5288 + ') clamping to ' + payload.max_physical_memory);
5289 payload.max_locked_memory = payload.max_physical_memory;
5290 }
5291 } else if (vmobj.hasOwnProperty('max_physical_memory')) {
5292 // new payload doesn't have a max_physical, so clamp to vmobj's
5293 if (payload.max_locked_memory > vmobj.max_physical_memory) {
5294 log.warn('max_locked_memory (' + payload.max_locked_memory
5295 + ') > vm.max_physical_memory (' + vmobj.max_physical_memory
5296 + ') clamping to ' + vmobj.max_physical_memory);
5297 payload.max_locked_memory = vmobj.max_physical_memory;
5298 }
5299 }
5300 }
5301
5302 if (payload.hasOwnProperty('max_swap')) {
5303 if (payload.hasOwnProperty('max_physical_memory')) {
5304 if (payload.max_swap < payload.max_physical_memory) {
5305 log.warn('max_swap (' + payload.max_swap
5306 + ') < max_physical_memory (' + payload.max_physical_memory
5307 + ') raising to ' + payload.max_physical_memory);
5308 payload.max_swap = payload.max_physical_memory;
5309 }
5310 } else if (vmobj.hasOwnProperty('max_physical_memory')) {
5311 // new payload doesn't have a max_physical, so raise to vmobj's
5312 if (payload.max_swap < vmobj.max_physical_memory) {
5313 log.warn('max_swap (' + payload.max_swap
5314 + ') < vm.max_physical_memory (' + vmobj.max_physical_memory
5315 + ') raising to ' + vmobj.max_physical_memory);
5316 payload.max_swap = vmobj.max_physical_memory;
5317 }
5318 }
5319 }
5320 }
5321
5322 // generate a new UUID if payload doesn't have one (also ensures that this uuid
5323 // does not already belong to a zone).
5324 function createZoneUUID(payload, log, callback)
5325 {
5326 var uuid;
5327
5328 assert(log, 'no logger passed to createZoneUUID()');
5329
5330 if (payload.hasOwnProperty('uuid')) {
5331 // Ensure that the uuid is not already used.
5332 getZoneRecords(null, log, function (err, records) {
5333 if (err) {
5334 callback(err);
5335 } else {
5336 if (records.hasOwnProperty(payload.uuid)) {
5337 callback(new Error('vm with UUID ' + payload.uuid
5338 + ' already exists.'));
5339 } else {
5340 callback(null, payload.uuid);
5341 }
5342 }
5343 });
5344 } else {
5345 log.debug('/usr/bin/uuid -v 4');
5346 execFile('/usr/bin/uuid', ['-v', '4'], function (err, stdout, stderr) {
5347 if (err) {
5348 callback(err);
5349 return;
5350 }
5351
5352 // chomp trailing spaces and newlines
5353 uuid = stdout.toString().replace(/\s+$/g, '');
5354 payload.uuid = uuid;
5355 log.info('generated uuid ' + uuid + ' for new VM');
5356 getZoneRecords(null, log, function (e, records) {
5357 if (e) {
5358 callback(e);
5359 } else {
5360 if (records.hasOwnProperty(payload.uuid)) {
5361 callback(new Error('vm with UUID ' + payload.uuid
5362 + 'already exists.'));
5363 } else {
5364 callback(null, payload.uuid);
5365 }
5366 }
5367 });
5368 });
5369 }
5370 }
5371
5372 function applyZoneDefaults(payload, log)
5373 {
5374 var allowed;
5375 var disk;
5376 var disks;
5377 var n;
5378 var nic;
5379 var nics;
5380 var zvol;
5381
5382 assert(log, 'no logger passed to applyZoneDefaults()');
5383
5384 log.debug('applying zone defaults');
5385
5386 if (!payload.hasOwnProperty('owner_uuid')) {
5387 // We assume that this all-zero uuid can be treated as 'admin'
5388 payload.owner_uuid = '00000000-0000-0000-0000-000000000000';
5389 }
5390
5391 if (!payload.hasOwnProperty('autoboot')) {
5392 payload.autoboot = true;
5393 }
5394
5395 if (!payload.hasOwnProperty('brand')) {
5396 payload.brand = 'joyent';
5397 }
5398
5399 if (!payload.hasOwnProperty('zpool')) {
5400 payload.zpool = 'zones';
5401 }
5402
5403 if (!payload.hasOwnProperty('dns_domain')) {
5404 payload.dns_domain = 'local';
5405 }
5406
5407 if (!payload.hasOwnProperty('cpu_shares')) {
5408 payload.cpu_shares = 100;
5409 } else {
5410 if (payload.cpu_shares > 65535) {
5411 log.info('capping cpu_shares at 64k (was: '
5412 + payload.cpu_shares + ')');
5413 payload.cpu_shares = 65535; // max is 64K
5414 }
5415 }
5416
5417 if (!payload.hasOwnProperty('zfs_io_priority')) {
5418 payload.zfs_io_priority = 100;
5419 }
5420
5421 if (!payload.hasOwnProperty('max_lwps')) {
5422 payload.max_lwps = 2000;
5423 }
5424
5425 // We need to set the RAM here because we use it as the default for
5426 // the max_physical_memory below. If we've set max_phys and we're not
5427 // KVM, we'll use that instead of ram anyway.
5428 if (!payload.hasOwnProperty('ram')) {
5429 payload.ram = 256;
5430 }
5431
5432 fixPayloadMemory(payload, {}, log);
5433
5434 allowed = BRAND_OPTIONS[payload.brand].allowed_properties;
5435 if (allowed.hasOwnProperty('vcpus') && !payload.hasOwnProperty('vcpus')) {
5436 payload.vcpus = 1;
5437 }
5438
5439 if (BRAND_OPTIONS[payload.brand].features.use_tmpfs
5440 && (!payload.hasOwnProperty('tmpfs')
5441 || (Number(payload.tmpfs) > Number(payload.max_physical_memory)))) {
5442
5443 payload.tmpfs = payload.max_physical_memory;
5444 }
5445
5446 if (!payload.hasOwnProperty('limit_priv')) {
5447 // note: the limit privs are going to be added to the brand and
5448 // shouldn't need to be set here by default when that's done.
5449 if (BRAND_OPTIONS[payload.brand].features.limit_priv) {
5450 payload.limit_priv
5451 = BRAND_OPTIONS[payload.brand].features.limit_priv.join(',');
5452 } else {
5453 payload.limit_priv = 'default';
5454 }
5455 }
5456
5457 if (!payload.hasOwnProperty('quota')) {
5458 payload.quota = '10'; // in GiB
5459 }
5460
5461 if (!payload.hasOwnProperty('billing_id')) {
5462 payload.billing_id = '00000000-0000-0000-0000-000000000000';
5463 }
5464
5465 if (payload.hasOwnProperty('add_disks')) {
5466 // update
5467 disks = payload.add_disks;
5468 } else if (payload.hasOwnProperty('disks')) {
5469 disks = payload.disks;
5470 } else {
5471 // no disks at all
5472 disks = [];
5473 }
5474
5475 for (disk in disks) {
5476 if (disks.hasOwnProperty(disk)) {
5477 zvol = disks[disk];
5478 if (!zvol.hasOwnProperty('model')
5479 && payload.hasOwnProperty('disk_driver')) {
5480
5481 zvol.model = payload.disk_driver;
5482 }
5483 if (!zvol.hasOwnProperty('media')) {
5484 zvol.media = 'disk';
5485 }
5486 }
5487 }
5488
5489 if (payload.hasOwnProperty('add_nics')) {
5490 // update
5491 nics = payload.add_nics;
5492 } else if (payload.hasOwnProperty('nics')) {
5493 nics = payload.nics;
5494 } else {
5495 // no disks at all
5496 nics = [];
5497 }
5498
5499 for (nic in nics) {
5500 if (nics.hasOwnProperty(nic)) {
5501 n = nics[nic];
5502 if (!n.hasOwnProperty('model')
5503 && payload.hasOwnProperty('nic_driver')) {
5504
5505 n.model = payload.nic_driver;
5506 }
5507 }
5508 }
5509 }
5510
5511 function validRecordSize(candidate)
5512 {
5513 if (candidate < 512) {
5514 // too low
5515 return (false);
5516 } else if (candidate > 131072) {
5517 // too high
5518 return (false);
5519 } else if ((candidate & (candidate - 1)) !== 0) {
5520 // not a power of 2
5521 return (false);
5522 }
5523
5524 return (true);
5525 }
5526
5527 // This function gets called for both create and update to check that payload
5528 // properties are reasonable. If vmobj is null, create is assumed, otherwise
5529 // update is assumed.
5530 function checkPayloadProperties(payload, vmobj, log, callback)
5531 {
5532 var array_fields = [
5533 'add_nics', 'update_nics', 'remove_nics',
5534 'add_disks', 'update_disks', 'remove_disks',
5535 'add_filesystems', 'update_filesystems', 'remove_filesystems'
5536 ];
5537 var changed_nics = [];
5538 var current_ips = [];
5539 var current_macs = [];
5540 var current_primary_ips = [];
5541 var current_vrids = [];
5542 var disk;
5543 var dst;
5544 var field;
5545 var filesys;
5546 var i;
5547 var ips = [];
5548 var is_nic = false;
5549 var live_ok;
5550 var mac;
5551 var macs = [];
5552 var m;
5553 var n;
5554 var nic;
5555 var nics_result = {};
5556 var nics_result_ordered = [];
5557 var nic_fields = ['add_nics', 'update_nics'];
5558 var only_vrrp_nics = true;
5559 var primary_nics;
5560 var prop;
5561 var props;
5562 var ram;
5563 var route;
5564 var routes_result = {};
5565 var brand;
5566 var vrids = [];
5567 var zvol;
5568
5569 assert(log, 'no logger passed to checkPayloadProperties()');
5570
5571 if (vmobj) {
5572 brand = vmobj.brand;
5573 } else if (payload.hasOwnProperty('brand')) {
5574 brand = payload.brand;
5575 } else {
5576 callback(new Error('unable to determine brand for VM'));
5577 }
5578
5579 /* check types of fields that should be arrays */
5580 for (field in array_fields) {
5581 field = array_fields[field];
5582 if (payload.hasOwnProperty(field) && ! Array.isArray(payload[field])) {
5583 callback(new Error(field + ' must be an array.'));
5584 return;
5585 }
5586 }
5587
5588 if (!vmobj) {
5589 // This is a CREATE
5590
5591 // These should have already been enforced
5592 if (payload.max_locked_memory > payload.max_physical_memory) {
5593 callback(new Error('max_locked_memory must be <= '
5594 + 'max_physical_memory'));
5595 return;
5596 }
5597 if (payload.max_swap < payload.max_physical_memory) {
5598 callback(new Error('max_swap must be >= max_physical_memory'));
5599 return;
5600 }
5601
5602 // We used to use zone_path instead of zonepath, so accept that too.
5603 if (payload.hasOwnProperty('zone_path')
5604 && !payload.hasOwnProperty('zonepath')) {
5605
5606 payload.zonepath = payload.zone_path;
5607 delete payload.zone_path;
5608 }
5609 } else {
5610 // This is an UPDATE
5611
5612 // can't update disks of a running VM
5613 if (payload.hasOwnProperty('add_disks')
5614 || payload.hasOwnProperty('remove_disks')) {
5615
5616 if ((vmobj.state !== 'stopped')
5617 || (vmobj.state === 'provisioning'
5618 && vmobj.zone_state !== 'installed')) {
5619
5620 callback(new Error('updates to disks are only allowed when '
5621 + 'state is "stopped", currently: ' + vmobj.state + ' ('
5622 + vmobj.zone_state + ')'));
5623 return;
5624 }
5625 }
5626
5627 // For update_disks we can update refreservation and compression values
5628 // while running. If there are other parameters to update though we'll
5629 // reject.
5630 if (payload.hasOwnProperty('update_disks')) {
5631 if ((vmobj.state !== 'stopped')
5632 || (vmobj.state === 'provisioning'
5633 && vmobj.zone_state !== 'installed')) {
5634
5635 live_ok = true;
5636
5637 payload.update_disks.forEach(function (d) {
5638 var key;
5639 var keys = Object.keys(d);
5640
5641 while ((keys.length > 0) && live_ok) {
5642 key = keys.pop();
5643 if ([
5644 'compression',
5645 'path',
5646 'refreservation'
5647 ].indexOf(key) === -1) {
5648
5649 // this key is not allowed!
5650 live_ok = false;
5651 }
5652 }
5653 });
5654
5655 if (!live_ok) {
5656 callback(new Error('at least one specified update to disks '
5657 + 'is only allowed when state is "stopped", currently: '
5658 + vmobj.state + ' (' + vmobj.zonestate + ')'));
5659 return;
5660 }
5661 }
5662 }
5663
5664 // if there's a min_overhead we ensure values are higher than ram.
5665 if (BRAND_OPTIONS[brand].features.min_memory_overhead) {
5666 if (payload.hasOwnProperty('ram')) {
5667 ram = payload.ram;
5668 } else {
5669 ram = vmobj.ram;
5670 }
5671
5672 // ensure none of these is < ram
5673 if (payload.hasOwnProperty('max_physical_memory')
5674 && payload.max_physical_memory < ram) {
5675
5676 callback(new Error('vm.max_physical_memory ('
5677 + payload.max_physical_memory + ') cannot be lower than'
5678 + ' vm.ram (' + ram + ')'));
5679 return;
5680 }
5681 if (payload.hasOwnProperty('max_locked_memory')
5682 && payload.max_locked_memory < ram) {
5683
5684 callback(new Error('vm.max_locked_memory ('
5685 + payload.max_locked_memory + ') cannot be lower than'
5686 + ' vm.ram (' + ram + ')'));
5687 return;
5688 }
5689 // This should not be allowed anyway because max_swap will be raised
5690 // to match max_physical_memory if you set it lower.
5691 if (payload.hasOwnProperty('max_swap')) {
5692 if (payload.max_swap < ram) {
5693 callback(new Error('vm.max_swap ('
5694 + payload.max_swap + ') cannot be lower than'
5695 + ' vm.ram (' + ram + ')'));
5696 return;
5697 } else if (payload.max_swap < MINIMUM_MAX_SWAP) {
5698 callback(new Error('vm.max_swap ('
5699 + payload.max_swap + ') cannot be lower than '
5700 + MINIMUM_MAX_SWAP + 'MiB'));
5701 return;
5702 }
5703 }
5704 }
5705
5706 /*
5707 * keep track of current IPs/MACs so we can make sure they're not being
5708 * duplicated.
5709 *
5710 */
5711 for (nic in vmobj.nics) {
5712 nic = vmobj.nics[nic];
5713 if (nic.hasOwnProperty('ip') && nic.ip !== 'dhcp') {
5714 current_ips.push(nic.ip);
5715 }
5716 if (nic.hasOwnProperty('mac')) {
5717 current_macs.push(nic.mac);
5718 }
5719 if (nic.hasOwnProperty('vrrp_vrid')) {
5720 current_vrids.push(nic.vrrp_vrid);
5721 }
5722 if (nic.hasOwnProperty('vrrp_primary_ip')) {
5723 current_primary_ips.push(nic.vrrp_primary_ip);
5724 }
5725
5726 if (nic.hasOwnProperty('mac') || nic.hasOwnProperty('vrrp_vrid')) {
5727 mac = nic.hasOwnProperty('mac') ? nic.mac
5728 : vrrpMAC(nic.vrrp_vrid);
5729 if (!nics_result.hasOwnProperty(mac)) {
5730 nics_result[mac] = nic;
5731 nics_result_ordered.push(nic);
5732 }
5733 }
5734 }
5735
5736 // Keep track of route additions / deletions, to make sure that
5737 // we're not setting link-local routes against nics that don't exist
5738 for (route in vmobj.routes) {
5739 routes_result[route] = vmobj.routes[route];
5740 }
5741 }
5742
5743 if (payload.hasOwnProperty('add_disks')) {
5744 for (disk in payload.add_disks) {
5745 if (payload.add_disks.hasOwnProperty(disk)) {
5746 zvol = payload.add_disks[disk];
5747
5748 // path is only allowed in 2 cases when adding a disk:
5749 //
5750 // 1) for cdrom devices
5751 // 2) when nocreate is specified
5752 //
5753 if (zvol.hasOwnProperty('path')) {
5754 if (zvol.media !== 'cdrom' && !zvol.nocreate) {
5755 callback(new Error('you cannot specify a path for a '
5756 + 'disk unless you set nocreate=true'));
5757 return;
5758 }
5759 }
5760
5761 // NOTE: We'll have verified the .zpool argument is a valid
5762 // zpool using VM.validate() if it's set.
5763
5764 if (zvol.hasOwnProperty('block_size')
5765 && !validRecordSize(zvol.block_size)) {
5766
5767 callback(new Error('invalid .block_size(' + zvol.block_size
5768 + '), must be 512-131072 and a power of 2.'));
5769 return;
5770 }
5771
5772 if (zvol.hasOwnProperty('block_size')
5773 && zvol.hasOwnProperty('image_uuid')) {
5774
5775 callback(new Error('setting both .block_size and '
5776 + '.image_uuid on a volume is invalid'));
5777 }
5778
5779 if (zvol.hasOwnProperty('compression')) {
5780 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) {
5781 callback(new Error('invalid compression setting for '
5782 + 'disk, must be one of: '
5783 + VM.COMPRESSION_TYPES.join(', ')));
5784 }
5785 }
5786
5787 if (!zvol.hasOwnProperty('model')
5788 || zvol.model === 'undefined') {
5789
5790 if (vmobj && vmobj.hasOwnProperty('disk_driver')) {
5791 zvol.model = vmobj.disk_driver;
5792 log.debug('set model to ' + zvol.model
5793 + ' from disk_driver');
5794 } else if (vmobj && vmobj.hasOwnProperty('disks')
5795 && vmobj.disks.length > 0 && vmobj.disks[0].model) {
5796
5797 zvol.model = vmobj.disks[0].model;
5798 log.debug('set model to ' + zvol.model + ' from disk0');
5799 } else {
5800 callback(new Error('missing .model option for '
5801 + 'disk: ' + JSON.stringify(zvol)));
5802 return;
5803 }
5804 } else if (VM.DISK_MODELS.indexOf(zvol.model) === -1) {
5805 callback(new Error('"' + zvol.model + '"'
5806 + ' is not a valid disk model. Valid are: '
5807 + VM.DISK_MODELS.join(',')));
5808 return;
5809 }
5810 }
5811 }
5812 }
5813
5814 if (payload.hasOwnProperty('update_disks')) {
5815 for (disk in payload.update_disks) {
5816 if (payload.update_disks.hasOwnProperty(disk)) {
5817 zvol = payload.update_disks[disk];
5818
5819 if (zvol.hasOwnProperty('compression')) {
5820 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) {
5821 callback(new Error('invalid compression type for '
5822 + 'disk, must be one of: '
5823 + VM.COMPRESSION_TYPES.join(', ')));
5824 }
5825 }
5826
5827 if (zvol.hasOwnProperty('block_size')) {
5828 callback(new Error('cannot change .block_size for a disk '
5829 + 'after creation'));
5830 return;
5831 }
5832 }
5833 }
5834 }
5835
5836 // If we're receiving, we might not have the filesystem yet
5837 if (!payload.hasOwnProperty('transition')
5838 || payload.transition.transition !== 'receiving') {
5839
5840 for (filesys in payload.filesystems) {
5841 filesys = payload.filesystems[filesys];
5842 if (!fs.existsSync(filesys.source)) {
5843 callback(new Error('missing requested filesystem: '
5844 + filesys.source));
5845 return;
5846 }
5847 }
5848 }
5849
5850 if (payload.hasOwnProperty('default_gateway')
5851 && payload.default_gateway !== '') {
5852
5853 log.warn('DEPRECATED: default_gateway should no longer be used, '
5854 + 'instead set one NIC primary and use nic.gateway.');
5855 }
5856
5857 primary_nics = 0;
5858 for (field in nic_fields) {
5859 field = nic_fields[field];
5860 if (payload.hasOwnProperty(field)) {
5861 for (nic in payload[field]) {
5862 if (payload[field].hasOwnProperty(nic)) {
5863 n = payload[field][nic];
5864
5865 // MAC will always conflict in update, since that's the key
5866 if (field === 'add_nics' && n.hasOwnProperty('mac')) {
5867 if ((macs.indexOf(n.mac) !== -1)
5868 || current_macs.indexOf(n.mac) !== -1) {
5869
5870 callback(new Error('Cannot add multiple NICs with '
5871 + 'the same MAC: ' + n.mac));
5872 return;
5873 }
5874 macs.push(n.mac);
5875 }
5876
5877 if (field === 'add_nics' || field === 'update_nics') {
5878 if (n.hasOwnProperty('primary')) {
5879 if (n.primary !== true) {
5880 callback(new Error('invalid value for NIC\'s '
5881 + 'primary flag: ' + n.primary + ' (must be'
5882 + ' true)'));
5883 return;
5884 }
5885 primary_nics++;
5886 }
5887 changed_nics.push(n);
5888 }
5889
5890 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') {
5891 if (ips.indexOf(n.ip) !== -1
5892 || current_ips.indexOf(n.ip) !== -1) {
5893
5894 callback(new Error('Cannot add multiple NICs with '
5895 + 'the same IP: ' + n.ip));
5896 return;
5897 }
5898 ips.push(n.ip);
5899 }
5900
5901 if (n.hasOwnProperty('vrrp_vrid')) {
5902 if (current_vrids.indexOf(n.vrrp_vrid) !== -1
5903 || vrids.indexOf(n.vrrp_vrid) !== -1) {
5904 callback(new Error('Cannot add multiple NICs with '
5905 + 'the same VRID: ' + n.vrrp_vrid));
5906 return;
5907 }
5908 vrids.push(n.vrrp_vrid);
5909 }
5910
5911 if (field === 'add_nics'
5912 && n.hasOwnProperty('vrrp_vrid')
5913 && n.hasOwnProperty('mac')) {
5914 callback(
5915 new Error('Cannot set both mac and vrrp_vrid'));
5916 return;
5917 }
5918
5919 if (n.hasOwnProperty('vrrp_primary_ip')) {
5920 current_primary_ips.push(n.vrrp_primary_ip);
5921 }
5922
5923 if (BRAND_OPTIONS[brand].features.model_required
5924 && field === 'add_nics'
5925 && (!n.hasOwnProperty('model') || !n.model
5926 || n.model === 'undefined' || n.model.length === 0)) {
5927
5928
5929 if (vmobj && vmobj.hasOwnProperty('nic_driver')) {
5930 n.model = vmobj.nic_driver;
5931 log.debug('set model to ' + n.model
5932 + ' from nic_driver');
5933 } else if (vmobj && vmobj.hasOwnProperty('nics')
5934 && vmobj.nics.length > 0 && vmobj.nics[0].model) {
5935
5936 n.model = vmobj.nics[0].model;
5937 log.debug('set model to ' + n.model + ' from nic0');
5938 } else {
5939 callback(new Error('missing .model option for NIC: '
5940 + JSON.stringify(n)));
5941 return;
5942 }
5943 }
5944
5945 if (field === 'add_nics' && n.ip !== 'dhcp'
5946 && (!n.hasOwnProperty('netmask')
5947 || !net.isIPv4(n.netmask))) {
5948
5949 callback(new Error('invalid or missing .netmask option '
5950 + 'for NIC: ' + JSON.stringify(n)));
5951 return;
5952 }
5953
5954 if ((field === 'add_nics' || field === 'update_nics')
5955 && n.hasOwnProperty('ip') && n.ip !== 'dhcp'
5956 && !net.isIPv4(n.ip)) {
5957
5958 callback(new Error('invalid IP for NIC: '
5959 + JSON.stringify(n)));
5960 return;
5961 }
5962
5963 if (field === 'add_nics' && (!n.hasOwnProperty('nic_tag')
5964 || !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/))) {
5965
5966 callback(new Error('invalid or missing .nic_tag option '
5967 + 'for NIC: ' + JSON.stringify(n)));
5968 return;
5969 }
5970
5971 if (field === 'update_nics' && n.hasOwnProperty('model')
5972 && (!n.model || n.model === 'undefined'
5973 || n.model.length === 0)) {
5974
5975 callback(new Error('invalid .model option for NIC: '
5976 + JSON.stringify(n)));
5977 return;
5978 }
5979
5980 if (field === 'update_nics' && n.hasOwnProperty('netmask')
5981 && (!n.netmask || !net.isIPv4(n.netmask))) {
5982
5983 callback(new Error('invalid .netmask option for NIC: '
5984 + JSON.stringify(n)));
5985 return;
5986 }
5987
5988 if (field === 'update_nics' && n.hasOwnProperty('nic_tag')
5989 && !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/)) {
5990
5991 callback(new Error('invalid .nic_tag option for NIC: '
5992 + JSON.stringify(n)));
5993 return;
5994 }
5995
5996 if (n.hasOwnProperty('mac')
5997 || n.hasOwnProperty('vrrp_vrid')) {
5998 mac = n.hasOwnProperty('mac') ? n.mac
5999 : vrrpMAC(n.vrrp_vrid);
6000 if (nics_result.hasOwnProperty(mac)) {
6001 var p;
6002 for (p in n) {
6003 nics_result[mac][p] = n[p];
6004 }
6005
6006 nics_result_ordered.forEach(function (on) {
6007 if (on.hasOwnProperty('mac') && on.mac == mac) {
6008 for (p in n) {
6009 on[p] = n[p];
6010 }
6011 }
6012 });
6013 } else {
6014 nics_result[mac] = n;
6015 nics_result_ordered.push(n);
6016 }
6017 }
6018
6019 if ((field === 'add_nics' || field === 'update_nics')
6020 && n.hasOwnProperty('allowed_ips')) {
6021 try {
6022 validateIPlist(n.allowed_ips);
6023 } catch (ipListErr) {
6024 callback(ipListErr);
6025 return;
6026 }
6027 }
6028 }
6029 }
6030 }
6031 }
6032
6033 if (payload.hasOwnProperty('remove_nics')) {
6034 for (m in payload.remove_nics) {
6035 m = payload.remove_nics[m];
6036 n = nics_result[m];
6037 if (!n) {
6038 continue;
6039 }
6040 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') {
6041 i = ips.indexOf(n.ip);
6042 if (i !== -1) {
6043 ips.splice(i, 1);
6044 }
6045 i = current_ips.indexOf(n.ip);
6046 if (i !== -1) {
6047 current_ips.splice(i, 1);
6048 }
6049 }
6050 delete nics_result[m];
6051
6052 for (i in nics_result_ordered) {
6053 n = nics_result_ordered[i];
6054 if (n.hasOwnProperty('mac') && n.mac == m) {
6055 nics_result_ordered.splice(i, 1);
6056 break;
6057 }
6058 }
6059 }
6060 }
6061
6062 // nics_result now has the state of the nics after the update - now check
6063 // properties that depend on each other or on other nics
6064 for (n in nics_result) {
6065 n = nics_result[n];
6066 if (n.hasOwnProperty('vrrp_vrid')) {
6067 if (n.hasOwnProperty('ip')
6068 && current_primary_ips.indexOf(n.ip) !== -1) {
6069 callback(
6070 new Error(
6071 'Cannot set vrrp_primary_ip to the IP of a VRRP nic'));
6072 return;
6073 }
6074
6075 if (!n.hasOwnProperty('vrrp_primary_ip')) {
6076 callback(new Error(
6077 'vrrp_vrid set but not vrrp_primary_ip'));
6078 return;
6079 }
6080 } else {
6081 only_vrrp_nics = false;
6082 }
6083 }
6084
6085 if (only_vrrp_nics && Object.keys(nics_result).length !== 0) {
6086 callback(new Error('VM cannot contain only VRRP nics'));
6087 return;
6088 }
6089
6090 for (i in current_primary_ips) {
6091 i = current_primary_ips[i];
6092 if ((current_ips.indexOf(i) === -1)
6093 && (ips.indexOf(i) === -1)) {
6094 callback(new Error(
6095 'vrrp_primary_ip must belong to the same VM'));
6096 return;
6097 }
6098 }
6099
6100 // Since we always need a primary nic, don't allow a value other than true
6101 // for primary flag. Also ensure we're not trying to set primary for more
6102 // than one nic.
6103 if (primary_nics > 1) {
6104 callback(new Error('payload specifies more than 1 primary NIC'));
6105 return;
6106 }
6107
6108 if (payload.hasOwnProperty('vga')
6109 && VM.VGA_TYPES.indexOf(payload.vga) === -1) {
6110
6111 callback(new Error('Invalid VGA type: "' + payload.vga
6112 + '", supported types are: ' + VM.VGA_TYPES.join(',')));
6113 return;
6114 }
6115
6116 function validLocalRoute(r) {
6117 var nicIdx = r.match(/nics\[(\d+)\]/);
6118 if (!nicIdx) {
6119 is_nic = false;
6120 return false;
6121 }
6122 is_nic = true;
6123
6124 if (nics_result_ordered.length === 0) {
6125 return false;
6126 }
6127
6128 nicIdx = Number(nicIdx[1]);
6129 if (!nics_result_ordered[nicIdx]
6130 || !nics_result_ordered[nicIdx].hasOwnProperty('ip')
6131 || nics_result_ordered[nicIdx].ip === 'dhcp') {
6132 return false;
6133 }
6134
6135 return true;
6136 }
6137
6138 props = [ 'routes', 'set_routes' ];
6139 for (prop in props) {
6140 prop = props[prop];
6141 if (payload.hasOwnProperty(prop)) {
6142 for (dst in payload[prop]) {
6143 var src = payload[prop][dst];
6144
6145 if (!net.isIPv4(dst) && !isCIDR(dst)) {
6146 callback(new Error('Invalid route destination: "' + dst
6147 + '" (must be IP address or CIDR)'));
6148 return;
6149 }
6150
6151 if (!net.isIPv4(src) && !validLocalRoute(src)) {
6152 callback(new Error(
6153 is_nic ? 'Route gateway: "' + src
6154 + '" refers to non-existent or DHCP nic'
6155 : 'Invalid route gateway: "' + src
6156 + '" (must be IP address or nic)'));
6157 return;
6158 }
6159
6160 routes_result[dst] = src;
6161 }
6162 }
6163 }
6164
6165 if (payload.hasOwnProperty('remove_routes')) {
6166 for (dst in payload.remove_routes) {
6167 dst = payload.remove_routes[dst];
6168 delete routes_result[dst];
6169 }
6170 }
6171
6172 // Now that we've applied all updates to routes, make sure that all
6173 // link-local routes refer to a nic that still exists
6174 for (dst in routes_result) {
6175 if (!net.isIPv4(routes_result[dst])
6176 && !validLocalRoute(routes_result[dst])) {
6177 callback(new Error('Route gateway: "' + routes_result[dst]
6178 + '" refers to non-existent or DHCP nic'));
6179 return;
6180 }
6181 }
6182
6183 // Ensure password is not too long
6184 if (payload.hasOwnProperty('vnc_password')
6185 && payload.vnc_password.length > 8) {
6186
6187 callback(new Error('VNC password is too long, maximum length is 8 '
6188 + 'characters.'));
6189 return;
6190 }
6191
6192 props = ['zfs_root_recsize', 'zfs_data_recsize'];
6193 for (prop in props) {
6194 prop = props[prop];
6195 if (payload.hasOwnProperty(prop)) {
6196 if (payload[prop] === 0 || payload[prop] === '') {
6197 // this is the default, so set it back to that.
6198 payload[prop] = 131072;
6199 } else if (!validRecordSize(payload[prop])) {
6200 callback(new Error('invalid ' + prop + ' (' + payload[prop]
6201 + '), must be 512-131072 and a power of 2. '
6202 + '(0 to disable)'));
6203 return;
6204 }
6205 }
6206 }
6207 props = ['zfs_root_compression', 'zfs_data_compression'];
6208 for (prop in props) {
6209 prop = props[prop];
6210
6211 if (payload.hasOwnProperty(prop)) {
6212 if (VM.COMPRESSION_TYPES.indexOf(payload[prop]) === -1) {
6213 callback(new Error('invalid compression type for '
6214 + payload[prop] + ', must be one of: '
6215 + VM.COMPRESSION_TYPES.join(', ')));
6216 }
6217 }
6218 }
6219
6220 // Ensure MACs and IPs are not already used on this vm
6221 // NOTE: can't check other nodes yet.
6222
6223 async.series([
6224 function (cb) {
6225 lookupConflicts(macs, ips, vrids, log, function (error, conflict) {
6226 if (error) {
6227 cb(error);
6228 } else {
6229 if (conflict) {
6230 cb(new Error('Conflict detected with another '
6231 + 'vm, please check the MAC, IP, and VRID'));
6232 } else {
6233 log.debug('no conflicts');
6234 cb();
6235 }
6236 }
6237 });
6238 }, function (cb) {
6239 lookupInvalidNicTags(changed_nics, log, function (e) {
6240 if (e) {
6241 cb(e);
6242 } else {
6243 cb();
6244 }
6245 });
6246 }, function (cb) {
6247 // We only allow adding firewall rules on create
6248 if (vmobj) {
6249 log.debug('update: not validating firewall data');
6250 cb();
6251 return;
6252 }
6253
6254 if (!payload.hasOwnProperty('firewall')) {
6255 log.debug('no firewall data in payload: not validating');
6256 cb();
6257 return;
6258 }
6259 validateFirewall(payload, log, cb);
6260 }
6261 ], function (err) {
6262 log.trace('leaving checkPayloadProperties()');
6263 callback(err);
6264 });
6265 }
6266
6267 function createDelegatedDataset(payload, log, callback)
6268 {
6269 var args;
6270 var ds;
6271 var zcfg = '';
6272
6273 assert(log, 'no logger passed to createDelegatedDataset()');
6274
6275 if (payload.delegate_dataset) {
6276 log.info('creating delegated dataset.');
6277 if (!payload.hasOwnProperty('zfs_filesystem')) {
6278 callback(new Error('payload missing zfs_filesystem'));
6279 return;
6280 }
6281 ds = path.join(payload.zfs_filesystem, '/data');
6282
6283 args = ['create'];
6284 if (payload.hasOwnProperty('zfs_data_compression')) {
6285 args.push('-o', 'compression=' + payload.zfs_data_compression);
6286 }
6287 if (payload.hasOwnProperty('zfs_data_recsize')) {
6288 args.push('-o', 'recsize=' + payload.zfs_data_recsize);
6289 }
6290 args.push(ds);
6291
6292 zfs(args, log, function (err) {
6293 if (err) {
6294 callback(err);
6295 return;
6296 }
6297
6298 zcfg = zcfg + 'add dataset; set name=' + ds + '; end\n';
6299 zonecfg(['-u', payload.uuid, zcfg], log, function (e, fds) {
6300 if (e) {
6301 log.error({'err': e, stdout: fds.stdout,
6302 stderr: fds.stderr}, 'unable to add delegated dataset '
6303 + ds + ' to ' + payload.uuid);
6304 callback(e);
6305 } else {
6306 log.debug({stdout: fds.stdout, stderr: fds.stderr},
6307 'added delegated dataset ' + ds);
6308 callback();
6309 }
6310 });
6311 });
6312 } else {
6313 callback();
6314 }
6315 }
6316
6317 function buildAddRemoveList(vmobj, payload, type, key, updatable)
6318 {
6319 var add = [];
6320 var add_key;
6321 var field;
6322 var newobj;
6323 var oldobj;
6324 var plural = type + 's';
6325 var remove = [];
6326 var remove_key;
6327 var update_key;
6328
6329 // initialize some plurals
6330 add_key = 'add_' + plural;
6331 remove_key = 'remove_' + plural;
6332 update_key = 'update_' + plural;
6333
6334 // There's no way to update properties on a disk or nic with zonecfg
6335 // currently. Yes, really. So any disks/nics that should be updated, we
6336 // remove then add with the new properties.
6337 if (payload.hasOwnProperty(update_key)) {
6338 for (newobj in payload[update_key]) {
6339 newobj = payload[update_key][newobj];
6340 for (oldobj in vmobj[plural]) {
6341 oldobj = vmobj[plural][oldobj];
6342
6343 if (oldobj[key] === newobj[key]) {
6344 // This is the one to update: remove and add.
6345 remove.push(oldobj[key]);
6346
6347 // only some fields make sense to update.
6348 for (field in updatable) {
6349 field = updatable[field];
6350 if (newobj.hasOwnProperty(field)) {
6351 oldobj[field] = newobj[field];
6352 }
6353 }
6354
6355 add.push(oldobj);
6356 }
6357 }
6358 }
6359 }
6360
6361 if (payload.hasOwnProperty(remove_key)) {
6362 for (newobj in payload[remove_key]) {
6363 newobj = payload[remove_key][newobj];
6364 remove.push(newobj);
6365 }
6366 }
6367
6368 if (payload.hasOwnProperty(add_key)) {
6369 for (newobj in payload[add_key]) {
6370 newobj = payload[add_key][newobj];
6371 add.push(newobj);
6372 }
6373 }
6374
6375 return ({'add': add, 'remove': remove});
6376 }
6377
6378 function buildDiskZonecfg(vmobj, payload)
6379 {
6380 var add = [];
6381 var disk;
6382 var lists;
6383 var remove = [];
6384 var zcfg = '';
6385
6386 lists = buildAddRemoveList(vmobj, payload, 'disk', 'path',
6387 UPDATABLE_DISK_PROPS);
6388 remove = lists.remove;
6389 add = lists.add;
6390
6391 // remove is a list of disk paths, add a remove for each now.
6392 for (disk in remove) {
6393 disk = remove[disk];
6394 zcfg = zcfg + 'remove -F device match=' + disk + '\n';
6395 }
6396
6397 for (disk in add) {
6398 disk = add[disk];
6399
6400 zcfg = zcfg + 'add device\n'
6401 + 'set match=' + disk.path + '\n'
6402 + 'add property (name=boot, value="'
6403 + (disk.boot ? 'true' : 'false') + '")\n'
6404 + 'add property (name=model, value="' + disk.model + '")\n';
6405
6406 if (disk.hasOwnProperty('media')) {
6407 zcfg = zcfg
6408 + 'add property (name=media, value="'
6409 + disk.media + '")\n';
6410 }
6411
6412 if (disk.hasOwnProperty('image_size')) {
6413 zcfg = zcfg
6414 + 'add property (name=image-size, value="'
6415 + disk.image_size + '")\n';
6416 } else if (disk.hasOwnProperty('size')) {
6417 zcfg = zcfg + 'add property (name=size, value="'
6418 + disk.size + '")\n';
6419 }
6420
6421 if (disk.hasOwnProperty('image_uuid')) {
6422 zcfg = zcfg
6423 + 'add property (name=image-uuid, value="'
6424 + disk.image_uuid + '")\n';
6425 }
6426
6427 if (disk.hasOwnProperty('image_name')) {
6428 zcfg = zcfg + 'add property (name=image-name, value="'
6429 + disk.image_name + '")\n';
6430 }
6431
6432 zcfg = zcfg + 'end\n';
6433 }
6434
6435 return zcfg;
6436 }
6437
6438 function buildNicZonecfg(vmobj, payload)
6439 {
6440 var add;
6441 var lists;
6442 var matches;
6443 var n;
6444 var new_primary;
6445 var nic;
6446 var nic_idx = 0;
6447 var remove;
6448 var updated_primary;
6449 var used_nic_indexes = [];
6450 var zcfg = '';
6451
6452 if (vmobj.hasOwnProperty('nics')) {
6453 // check whether we're adding or updating to set the primary flag. If we
6454 // are also find the existing NIC with the primary flag. If that's not
6455 // being removed, update it to remove the primary flag.
6456 if (payload.hasOwnProperty('add_nics')) {
6457 for (nic in payload.add_nics) {
6458 nic = payload.add_nics[nic];
6459 if (nic.hasOwnProperty('primary')) {
6460 new_primary = nic.mac;
6461 }
6462 }
6463 }
6464 if (payload.hasOwnProperty('update_nics')) {
6465 for (nic in payload.update_nics) {
6466 nic = payload.update_nics[nic];
6467 if (nic.hasOwnProperty('primary')) {
6468 new_primary = nic.mac;
6469 }
6470 }
6471 }
6472 if (new_primary) {
6473 // find old primary
6474 for (nic in vmobj.nics) {
6475 nic = vmobj.nics[nic];
6476 if (nic.hasOwnProperty('primary') && nic.mac !== new_primary) {
6477 // we have a new primary, so un-primary the old.
6478 if (payload.hasOwnProperty('remove_nics')
6479 && payload.remove_nics.indexOf(nic.mac) !== -1) {
6480
6481 // we're removing the old primary so: done.
6482 break;
6483 } else if (payload.hasOwnProperty('update_nics')) {
6484 updated_primary = false;
6485 for (n in payload.update_nics) {
6486 n = payload.update_nics[n];
6487 if (n.mac === nic.mac) {
6488 n.primary = false;
6489 updated_primary = true;
6490 }
6491 }
6492 if (!updated_primary) {
6493 payload.update_nics.push({'mac': nic.mac,
6494 'primary': false});
6495 }
6496 } else {
6497 // just add a new update to unset the
6498 payload.update_nics =
6499 [ {'mac': nic.mac, 'primary': false} ];
6500 }
6501 }
6502 }
6503 }
6504 }
6505
6506 lists = buildAddRemoveList(vmobj, payload, 'nic', 'mac',
6507 UPDATABLE_NIC_PROPS);
6508 remove = lists.remove;
6509 add = lists.add;
6510
6511 // create a list of used indexes so we can find the free ones
6512 if (vmobj.hasOwnProperty('nics')) {
6513 for (n in vmobj.nics) {
6514 if (vmobj.nics[n].hasOwnProperty('interface')) {
6515 matches = vmobj.nics[n].interface.match(/^net(\d+)$/);
6516 if (matches) {
6517 used_nic_indexes.push(Number(matches[1]));
6518 }
6519 }
6520 }
6521 }
6522
6523 // assign next available interface for nics without one
6524 for (nic in add) {
6525 nic = add[nic];
6526 if (!nic.hasOwnProperty('interface')) {
6527 while (used_nic_indexes.indexOf(nic_idx) !== -1) {
6528 nic_idx++;
6529 }
6530 nic.interface = 'net' + nic_idx;
6531 used_nic_indexes.push(Number(nic_idx));
6532 }
6533
6534 // Changing the VRID changes the MAC address too, since the VRID is
6535 // encoded in the MAC. This can't be done until after
6536 // buildAddRemoveList above, since mac is used as the key to figure
6537 // out which nic is which
6538 if (nic.hasOwnProperty('vrrp_vrid')) {
6539 nic.mac = vrrpMAC(nic.vrrp_vrid);
6540 }
6541 }
6542
6543 // remove is a list of nic macs, add a remove for each now.
6544 for (nic in remove) {
6545 nic = remove[nic];
6546 zcfg = zcfg + 'remove net mac-addr=' + ruinMac(nic) + '\n';
6547 }
6548
6549 // properties that don't require any validation - add them if they're
6550 // present:
6551 var nicProperties = ['ip', 'netmask', 'network_uuid', 'model',
6552 'dhcp_server', 'allow_dhcp_spoofing', 'blocked_outgoing_ports',
6553 'allow_ip_spoofing', 'allow_mac_spoofing', 'allow_restricted_traffic',
6554 'allow_unfiltered_promisc', 'vrrp_vrid', 'vrrp_primary_ip'];
6555
6556 for (nic in add) {
6557 nic = add[nic];
6558
6559 zcfg = zcfg
6560 + 'add net\n'
6561 + 'set physical=' + nic.interface + '\n'
6562 + 'set mac-addr=' + ruinMac(nic.mac) + '\n';
6563
6564 if (nic.hasOwnProperty('nic_tag')) {
6565 zcfg = zcfg + 'set global-nic=' + nic.nic_tag + '\n';
6566 }
6567
6568 if (nic.hasOwnProperty('gateway') && nic.gateway.length > 0) {
6569 zcfg = zcfg + 'add property (name=gateway, value="'
6570 + nic.gateway + '")\n';
6571 }
6572
6573 if (nic.hasOwnProperty('primary') && nic.primary) {
6574 zcfg = zcfg + 'add property (name=primary, value="true")\n';
6575 }
6576
6577 if (nic.hasOwnProperty('vlan_id') && (nic.vlan_id !== '0')) {
6578 zcfg = zcfg + 'set vlan-id=' + nic.vlan_id + '\n';
6579 }
6580
6581 if (nic.hasOwnProperty('allowed_ips')) {
6582 zcfg = zcfg
6583 + 'add property (name=allowed_ips, value="'
6584 + nic.allowed_ips.join(',') + '")\n';
6585 }
6586
6587 for (var prop in nicProperties) {
6588 prop = nicProperties[prop];
6589 if (nic.hasOwnProperty(prop)) {
6590 zcfg = zcfg + 'add property (name=' + prop + ', value="'
6591 + nic[prop] + '")\n';
6592 }
6593 }
6594
6595 zcfg = zcfg + 'end\n';
6596 }
6597
6598 return zcfg;
6599 }
6600
6601 function buildFilesystemZonecfg(vmobj, payload)
6602 {
6603 var add = [];
6604 var filesystem;
6605 var lists;
6606 var opt;
6607 var remove = [];
6608 var zcfg = '';
6609
6610 lists = buildAddRemoveList(vmobj, payload, 'filesystem', 'target', []);
6611 remove = lists.remove;
6612 add = lists.add;
6613
6614 // remove is a list of disk paths, add a remove for each now.
6615 for (filesystem in remove) {
6616 filesystem = remove[filesystem];
6617 zcfg = zcfg + 'remove fs match=' + filesystem + '\n';
6618 }
6619
6620 for (filesystem in add) {
6621 filesystem = add[filesystem];
6622
6623 zcfg = zcfg + 'add fs\n' + 'set dir=' + filesystem.target + '\n'
6624 + 'set special=' + filesystem.source + '\n' + 'set type='
6625 + filesystem.type + '\n';
6626 if (filesystem.hasOwnProperty('raw')) {
6627 zcfg = zcfg + 'set raw=' + filesystem.raw + '\n';
6628 }
6629 if (filesystem.hasOwnProperty('options')) {
6630 for (opt in filesystem.options) {
6631 opt = filesystem.options[opt];
6632 zcfg = zcfg + 'add options "' + opt + '"\n';
6633 }
6634 }
6635 zcfg = zcfg + 'end\n';
6636 }
6637
6638 return zcfg;
6639 }
6640
6641 function buildZonecfgUpdate(vmobj, payload, log)
6642 {
6643 var brand;
6644 var tmp;
6645 var zcfg = '';
6646
6647 assert(log, 'no logger passed to buildZonecfgUpdate()');
6648
6649 log.debug({vmobj: vmobj, payload: payload},
6650 'parameters to buildZonecfgUpdate()');
6651
6652 if (vmobj && vmobj.hasOwnProperty('brand')) {
6653 brand = vmobj.brand;
6654 } else {
6655 brand = payload.brand;
6656 }
6657
6658 // Global properties can just be set, no need to clear anything first.
6659 if (payload.hasOwnProperty('cpu_shares')) {
6660 zcfg = zcfg + 'set cpu-shares=' + payload.cpu_shares.toString() + '\n';
6661 }
6662 if (payload.hasOwnProperty('zfs_io_priority')) {
6663 zcfg = zcfg + 'set zfs-io-priority='
6664 + payload.zfs_io_priority.toString() + '\n';
6665 }
6666 if (payload.hasOwnProperty('max_lwps')) {
6667 zcfg = zcfg + 'set max-lwps=' + payload.max_lwps.toString() + '\n';
6668 }
6669 if (payload.hasOwnProperty('limit_priv')) {
6670 zcfg = zcfg + 'set limitpriv="' + payload.limit_priv + '"\n';
6671 }
6672
6673 if (!BRAND_OPTIONS[brand].features.use_vm_autoboot
6674 && payload.hasOwnProperty('autoboot')) {
6675
6676 // kvm autoboot is managed by the vm-autoboot attr instead
6677 zcfg = zcfg + 'set autoboot=' + payload.autoboot.toString() + '\n';
6678 }
6679
6680 // Capped Memory properties are special
6681 if (payload.hasOwnProperty('max_physical_memory')
6682 || payload.hasOwnProperty('max_locked_memory')
6683 || payload.hasOwnProperty('max_swap')) {
6684
6685 // Capped memory parameters need either an add or select first.
6686 if (vmobj.hasOwnProperty('max_physical_memory')
6687 || vmobj.hasOwnProperty('max_locked_memory')
6688 || vmobj.hasOwnProperty('max_swap')) {
6689
6690 // there's already a capped-memory section, use that.
6691 zcfg = zcfg + 'select capped-memory; ';
6692 } else {
6693 zcfg = zcfg + 'add capped-memory; ';
6694 }
6695
6696 if (payload.hasOwnProperty('max_physical_memory')) {
6697 zcfg = zcfg + 'set physical='
6698 + payload.max_physical_memory.toString() + 'm; ';
6699 }
6700 if (payload.hasOwnProperty('max_locked_memory')) {
6701 zcfg = zcfg + 'set locked='
6702 + payload.max_locked_memory.toString() + 'm; ';
6703 }
6704 if (payload.hasOwnProperty('max_swap')) {
6705 zcfg = zcfg + 'set swap='
6706 + payload.max_swap.toString() + 'm; ';
6707 }
6708
6709 zcfg = zcfg + 'end\n';
6710 }
6711
6712 // Capped CPU is special
6713 if (payload.hasOwnProperty('cpu_cap')) {
6714 if (vmobj.hasOwnProperty('cpu_cap')) {
6715 zcfg = zcfg + 'select capped-cpu; ';
6716 } else {
6717 zcfg = zcfg + 'add capped-cpu; ';
6718 }
6719
6720 zcfg = zcfg + 'set ncpus='
6721 + (Number(payload.cpu_cap) * 0.01).toString() + '; end\n';
6722 }
6723
6724 // set to empty string so property is removed when not true or when not
6725 // false if that's the default for the property.
6726 if (payload.hasOwnProperty('do_not_inventory')) {
6727 if (payload.do_not_inventory !== true) {
6728 // removing sets false as that's the default.
6729 payload.do_not_inventory = '';
6730 }
6731 }
6732
6733 if (payload.hasOwnProperty('archive_on_delete')) {
6734 if (payload.archive_on_delete !== true) {
6735 // removing sets false as that's the default.
6736 payload.archive_on_delete = '';
6737 }
6738 }
6739
6740 if (payload.hasOwnProperty('firewall_enabled')) {
6741 if (payload.firewall_enabled !== true) {
6742 // removing sets false as that's the default.
6743 payload.firewall_enabled = '';
6744 }
6745 }
6746
6747 if (payload.hasOwnProperty('restart_init')) {
6748 if (payload.restart_init === true) {
6749 // removing sets true as that's the default.
6750 payload.restart_init = '';
6751 }
6752 }
6753
6754 // Attributes
6755 function setAttr(attr, attr_name, value) {
6756 if (!value) {
6757 value = payload[attr_name];
6758 }
6759
6760 if (payload.hasOwnProperty(attr_name)) {
6761 if ((typeof (value) !== 'boolean')
6762 && (!value || trim(value.toString()) === '')) {
6763
6764 // empty values we either remove or ignore.
6765 if (vmobj.hasOwnProperty(attr_name)) {
6766 zcfg = zcfg + 'remove attr name=' + attr + ';';
6767 // else do nothing, we don't add empty values.
6768 }
6769 } else {
6770 if (attr_name === 'resolvers'
6771 && vmobj.hasOwnProperty('resolvers')
6772 && vmobj.resolvers.length === 0) {
6773
6774 // special case for resolvers: we always have 'resolvers'
6775 // in the object, but if it's empty we don't have it in the
6776 // zonecfg. Add instead of the usual update.
6777 zcfg = zcfg + 'add attr; set name="' + attr + '"; '
6778 + 'set type=string; ';
6779 } else if (vmobj.hasOwnProperty(attr_name)) {
6780 zcfg = zcfg + 'select attr name=' + attr + '; ';
6781 } else {
6782 zcfg = zcfg + 'add attr; set name="' + attr + '"; '
6783 + 'set type=string; ';
6784 }
6785 zcfg = zcfg + 'set value="' + value.toString() + '"; end\n';
6786 }
6787 }
6788 }
6789 setAttr('billing-id', 'billing_id');
6790 setAttr('owner-uuid', 'owner_uuid');
6791 setAttr('package-name', 'package_name');
6792 setAttr('package-version', 'package_version');
6793 setAttr('tmpfs', 'tmpfs');
6794 setAttr('hostname', 'hostname');
6795 setAttr('dns-domain', 'dns_domain');
6796 setAttr('default-gateway', 'default_gateway');
6797 setAttr('do-not-inventory', 'do_not_inventory');
6798 setAttr('archive-on-delete', 'archive_on_delete');
6799 setAttr('firewall-enabled', 'firewall_enabled');
6800 setAttr('restart-init', 'restart_init');
6801 setAttr('init-name', 'init_name');
6802 setAttr('disk-driver', 'disk_driver');
6803 setAttr('nic-driver', 'nic_driver');
6804
6805 if (payload.hasOwnProperty('resolvers')) {
6806 setAttr('resolvers', 'resolvers', payload.resolvers.join(','));
6807 }
6808 if (payload.hasOwnProperty('alias')) {
6809 tmp = '';
6810 if (payload.alias) {
6811 tmp = new Buffer(payload.alias).toString('base64');
6812 }
6813 setAttr('alias', 'alias', tmp);
6814 }
6815
6816 if (BRAND_OPTIONS[brand].features.use_vm_autoboot) {
6817 setAttr('vm-autoboot', 'autoboot');
6818 }
6819
6820 // XXX Used on KVM but can be passed in for 'OS' too. We only setAttr on KVM
6821 if (BRAND_OPTIONS[brand].features.type === 'KVM') {
6822 setAttr('ram', 'ram');
6823 }
6824
6825 // NOTE: Thanks to normalizePayload() we'll only have these when relevant
6826 setAttr('vcpus', 'vcpus');
6827 setAttr('boot', 'boot');
6828 setAttr('cpu-type', 'cpu_type');
6829 setAttr('vga', 'vga');
6830 setAttr('vnc-port', 'vnc_port');
6831 setAttr('spice-port', 'spice_port');
6832 setAttr('virtio-txtimer', 'virtio_txtimer');
6833 setAttr('virtio-txburst', 'virtio_txburst');
6834
6835 // We use base64 here for these next five options:
6836 //
6837 // vnc_password
6838 // spice_password
6839 // spice_opts
6840 // qemu_opts
6841 // qemu_extra_opts
6842 //
6843 // since these can contain characters zonecfg doesn't like.
6844 //
6845 if (payload.hasOwnProperty('vnc_password')) {
6846 if (payload.vnc_password === ''
6847 && (vmobj.hasOwnProperty('vnc_password')
6848 && vmobj.vnc_password !== '')) {
6849
6850 log.warn('Warning: VNC password was removed for VM '
6851 + vmobj.uuid + ' but VM needs to be restarted for change to'
6852 + 'take effect.');
6853 }
6854 if (payload.vnc_password.length > 0
6855 && !vmobj.hasOwnProperty('vnc_password')) {
6856
6857 log.warn('Warning: VNC password was added to VM '
6858 + vmobj.uuid + ' but VM needs to be restarted for change to'
6859 + 'take effect.');
6860 }
6861
6862 setAttr('vnc-password', 'vnc_password',
6863 new Buffer(payload.vnc_password).toString('base64'));
6864 }
6865 if (payload.hasOwnProperty('spice_password')) {
6866 if (payload.spice_password === ''
6867 && (vmobj.hasOwnProperty('spice_password')
6868 && vmobj.spice_password !== '')) {
6869
6870 log.warn('Warning: SPICE password was removed for VM '
6871 + vmobj.uuid + ' but VM needs to be restarted for change to'
6872 + 'take effect.');
6873 }
6874 if (payload.spice_password.length > 0
6875 && !vmobj.hasOwnProperty('spice_password')) {
6876
6877 log.warn('Warning: SPICE password was added to VM '
6878 + vmobj.uuid + ' but VM needs to be restarted for change to'
6879 + 'take effect.');
6880 }
6881
6882 setAttr('spice-password', 'spice_password',
6883 new Buffer(payload.spice_password).toString('base64'));
6884 }
6885 if (payload.hasOwnProperty('spice_opts')) {
6886 setAttr('spice-opts', 'spice_opts',
6887 new Buffer(payload.spice_opts).toString('base64'));
6888 }
6889 if (payload.hasOwnProperty('qemu_opts')) {
6890 setAttr('qemu-opts', 'qemu_opts',
6891 new Buffer(payload.qemu_opts).toString('base64'));
6892 }
6893 if (payload.hasOwnProperty('qemu_extra_opts')) {
6894 setAttr('qemu-extra-opts', 'qemu_extra_opts',
6895 new Buffer(payload.qemu_extra_opts).toString('base64'));
6896 }
6897
6898 // Handle disks
6899 if (payload.hasOwnProperty('disks')
6900 || payload.hasOwnProperty('add_disks')
6901 || payload.hasOwnProperty('update_disks')
6902 || payload.hasOwnProperty('remove_disks')) {
6903
6904 zcfg = zcfg + buildDiskZonecfg(vmobj, payload);
6905 }
6906
6907 if (payload.hasOwnProperty('fs_allowed')) {
6908 if (payload.fs_allowed === '') {
6909 zcfg = zcfg + 'clear fs-allowed\n';
6910 } else {
6911 zcfg = zcfg + 'set fs-allowed="' + payload.fs_allowed + '"\n';
6912 }
6913 }
6914
6915 if (payload.hasOwnProperty('filesystems')
6916 || payload.hasOwnProperty('add_filesystems')
6917 || payload.hasOwnProperty('update_filesystems')
6918 || payload.hasOwnProperty('add_filesystems')) {
6919
6920 zcfg = zcfg + buildFilesystemZonecfg(vmobj, payload);
6921 }
6922
6923 zcfg = zcfg + buildNicZonecfg(vmobj, payload);
6924
6925 return zcfg;
6926 }
6927
6928 // Checks that QMP is responding to query-status and if so passes the boolean
6929 // value of the hwsetup parameter to the callback.
6930 //
6931 // vmobj must have:
6932 //
6933 // zonepath
6934 //
6935 function checkHWSetup(vmobj, log, callback)
6936 {
6937 var q;
6938 var socket;
6939
6940 assert(log, 'no logger passed to checkHWSetup()');
6941
6942 q = new Qmp(log);
6943 socket = vmobj.zonepath + '/root/tmp/vm.qmp';
6944
6945 q.connect(socket, function (error) {
6946 if (error) {
6947 log.error(error, 'q.connect(): Error: ' + error.message);
6948 callback(error);
6949 return;
6950 }
6951 q.command('query-status', null, function (e, result) {
6952 if (e) {
6953 log.error(e, 'q.command(query-status): Error: ' + e.message);
6954 callback(e);
6955 return;
6956 }
6957 q.disconnect();
6958 callback(null, result.hwsetup ? true : false);
6959 return;
6960 });
6961 });
6962 }
6963
6964 // cb (if set) will be called with an Error if we can't setup the interval loop
6965 // otherwise when the loop is shut down.
6966 //
6967 // vmobj must have:
6968 //
6969 // brand
6970 // state
6971 // uuid
6972 // zonepath
6973 // zoneroot
6974 //
6975 function markProvisionedWhenHWSetup(vmobj, options, cb)
6976 {
6977 var ival_handle;
6978 var log;
6979 var loop_interval = 3; // seconds
6980 var zoneroot;
6981
6982 log = options.log;
6983 assert(log, 'no logger passed to markProvisionedWenHWSetup()');
6984 assert(vmobj.hasOwnProperty('zonepath'), 'no zonepath in vmobj');
6985
6986 zoneroot = path.join(vmobj.zoneroot, '/root');
6987
6988 if (!BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) {
6989 // do nothing for zones where we don't wait for hwsetup
6990 cb(new Error('brand ' + vmobj.brand + ' does not support hwsetup'));
6991 return (null);
6992 }
6993
6994 // Ensure the dataset doesn't have unsafe links as /var or /var/svc
6995 // Since we're checking the 'file' provision_success, this also guarantees
6996 // that if it already exists, it's not a symlink.
6997 try {
6998 assertSafeZonePath(zoneroot, '/var/svc/provision_success',
6999 {type: 'file', enoent_ok: true});
7000 } catch (e) {
7001 cb(e);
7002 return (null);
7003 }
7004
7005 if (!options) {
7006 options = {};
7007 }
7008
7009 // if caller wants they can change the interval
7010 if (options.hasOwnProperty('interval')) {
7011 loop_interval = options.interval;
7012 }
7013
7014 log.debug('setting hwsetup interval ' + vmobj.uuid);
7015 ival_handle = setInterval(function () {
7016 VM.load(vmobj.uuid, {fields: ['transition_expire', 'uuid'], log: log},
7017 function (err, obj) {
7018
7019 var timeout_remaining;
7020 var ival = ival_handle;
7021
7022 function done() {
7023 if (ival_handle) {
7024 log.debug('clearing hwsetup interval ' + vmobj.uuid);
7025 clearInterval(ival);
7026 ival = null;
7027 } else {
7028 log.debug('done but no hwsetup interval ' + vmobj.uuid);
7029 }
7030 }
7031
7032 if (err) {
7033 // If the VM was deleted between calls, nothing much we can do.
7034 log.error(err, 'Unable to load ' + vmobj.uuid + ' '
7035 + err.message);
7036 done();
7037 cb(err);
7038 return;
7039 }
7040
7041 // we only do anything if we're still waiting for provisioning
7042 if (vmobj.state !== 'provisioning') {
7043 done();
7044 cb();
7045 return;
7046 }
7047
7048 timeout_remaining =
7049 (Number(obj.transition_expire) - Date.now(0)) / 1000;
7050
7051 if (timeout_remaining <= 0) {
7052 // IMPORTANT: this may run multiple times, must be idempotent
7053
7054 log.warn('Marking VM ' + vmobj.uuid + ' as "failed" because'
7055 + ' timeout expired and we are still "provisioning"');
7056 VM.markVMFailure(vmobj, {log: log}, function (mark_err) {
7057 log.warn(mark_err, 'zoneinit failed, zone is '
7058 + 'being stopped for manual investigation.');
7059 done();
7060 cb();
7061 });
7062 return;
7063 }
7064
7065 checkHWSetup(vmobj, log, function (check_err, result) {
7066 if (check_err) {
7067 log.debug(check_err, 'checkHWSetup Error: '
7068 + check_err.message);
7069 return;
7070 }
7071
7072 if (result) {
7073 log.debug('QMP says VM ' + vmobj.uuid
7074 + ' completed hwsetup');
7075 VM.unsetTransition(vmobj, {log: log}, function (unset_err) {
7076 var provisioning;
7077 var provision_success;
7078
7079 provisioning = path.join(vmobj.zonepath,
7080 '/root/var/svc/provisioning');
7081 provision_success = path.join(vmobj.zonepath,
7082 '/root/var/svc/provision_success');
7083
7084 if (unset_err) {
7085 log.error(unset_err);
7086 } else {
7087 log.debug('cleared transition to provisioning on'
7088 + ' ' + vmobj.uuid);
7089 }
7090
7091 fs.rename(provisioning, provision_success,
7092 function (e) {
7093
7094 if (e) {
7095 if (e.code === 'ENOENT') {
7096 log.debug(e);
7097 } else {
7098 log.error(e);
7099 }
7100 }
7101
7102 done();
7103 cb();
7104 return;
7105 });
7106 });
7107 }
7108 });
7109 });
7110 }, loop_interval * 1000);
7111
7112 return (ival_handle);
7113 }
7114
7115 function archiveVM(uuid, options, callback)
7116 {
7117 var archive_dirname;
7118 var dirmode;
7119 var log;
7120 var patterns_to_archive = [];
7121 var vmobj;
7122
7123 /*jsl:ignore*/
7124 dirmode = 0755;
7125 /*jsl:end*/
7126
7127 if (options.hasOwnProperty('log')) {
7128 log = options.log;
7129 } else {
7130 log = VM.log;
7131 }
7132
7133 log.debug('attempting to archive debug data for VM ' + uuid);
7134
7135 async.series([
7136 function (cb) {
7137 // ensure directory exists
7138 archive_dirname = path.join('/zones/archive', uuid);
7139
7140 fs.mkdir(archive_dirname, dirmode, function (e) {
7141 log.debug(e, 'attempted to create ' + archive_dirname);
7142 cb(e);
7143 return;
7144 });
7145 }, function (cb) {
7146 VM.load(uuid, {log: log}, function (err, obj) {
7147 if (err) {
7148 cb(err);
7149 return;
7150 }
7151 vmobj = obj;
7152 cb();
7153 });
7154 }, function (cb) {
7155 // write vmobj to archive
7156 var filename;
7157
7158 filename = path.join(archive_dirname, 'vm.json');
7159
7160 fs.writeFile(filename, JSON.stringify(vmobj, null, 2) + '\n',
7161 function (err, result) {
7162
7163 if (err) {
7164 log.error(err, 'failed to create ' + filename + ': '
7165 + err.message);
7166 } else {
7167 log.info('archived data to ' + filename);
7168 }
7169
7170 cb(); // ignore error
7171 });
7172 }, function (cb) {
7173 var cmdline = '/usr/sbin/zfs list -t all -o name | grep '
7174 + vmobj.zonename + ' | xargs zfs get -pH all >'
7175 + path.join(archive_dirname, 'zfs.dump');
7176
7177 log.debug(cmdline);
7178 exec(cmdline, function (e, stdout, stderr) {
7179 if (e) {
7180 e.stdout = stdout;
7181 e.stderr = stderr;
7182 log.error({err: e}, 'failed to create '
7183 + path.join(archive_dirname, 'zfs.dump'));
7184 cb(e);
7185 return;
7186 }
7187 log.info('archived data to ' + path.join(archive_dirname,
7188 'zfs.dump'));
7189 cb();
7190 });
7191 }, function (cb) {
7192 patterns_to_archive.push({
7193 src: path.join('/etc/zones/', vmobj.zonename + '.xml'),
7194 dst: path.join(archive_dirname, 'zone.xml')
7195 });
7196 patterns_to_archive.push({
7197 src: path.join(vmobj.zonepath, 'config'),
7198 dst: archive_dirname,
7199 targ: path.join(archive_dirname, 'config')
7200 });
7201 patterns_to_archive.push({
7202 src: path.join(vmobj.zonepath, 'cores'),
7203 dst: archive_dirname,
7204 targ: path.join(archive_dirname, 'cores')
7205 });
7206
7207 if (vmobj.brand === 'kvm') {
7208 patterns_to_archive.push({
7209 src: path.join(vmobj.zonepath, 'root/tmp/vm*.log*'),
7210 dst: path.join(archive_dirname, 'vmlogs'),
7211 create_dst_dir: true
7212 });
7213 patterns_to_archive.push({
7214 src: path.join(vmobj.zonepath, 'root/startvm'),
7215 dst: archive_dirname,
7216 targ: path.join(archive_dirname, 'startvm')
7217 });
7218 } else {
7219 patterns_to_archive.push({
7220 src: path.join(vmobj.zonepath, 'root/var/svc/log/*'),
7221 dst: path.join(archive_dirname, 'svclogs'),
7222 create_dst_dir: true
7223 });
7224 patterns_to_archive.push({
7225 src: path.join(vmobj.zonepath, 'root/var/adm/messages*'),
7226 dst: path.join(archive_dirname, 'admmsgs'),
7227 create_dst_dir: true
7228 });
7229 }
7230
7231 async.forEachSeries(patterns_to_archive, function (pattern, c) {
7232
7233 function cpPattern(p, cp_cb) {
7234 var cmdline = '/usr/bin/cp -RP ' + p.src + ' ' + p.dst;
7235 var targ = p.targ || p.dst;
7236
7237 log.debug(cmdline);
7238 exec(cmdline, function (e, stdout, stderr) {
7239 if (e) {
7240 e.stdout = stdout;
7241 e.stderr = stderr;
7242 log.error({err: e}, 'failed to archive data to '
7243 + targ);
7244 } else {
7245 log.info('archived data to ' + targ);
7246 }
7247 // we don't return errors here because on error copying
7248 // one pattern we still want to grab the others.
7249 cp_cb();
7250 });
7251 }
7252
7253 if (pattern.create_dst_dir) {
7254 fs.mkdir(pattern.dst, dirmode, function (e) {
7255 if (!e) {
7256 log.info('created ' + pattern.dst);
7257 } else {
7258 log.error({err: e}, 'failed to create '
7259 + pattern.dst);
7260 }
7261 cpPattern(pattern, c);
7262 });
7263 } else {
7264 cpPattern(pattern, c);
7265 }
7266 }, function (e) {
7267 log.info('finished archiving VM ' + vmobj.uuid);
7268 cb(e);
7269 });
7270 }
7271 ], function () {
7272 // XXX we ignore errors as failures to archive will not block VM delete.
7273 callback();
7274 });
7275 }
7276
7277 // vmobj argument should have:
7278 //
7279 // transition_to
7280 // uuid
7281 // zonename
7282 //
7283 exports.markVMFailure = function (vmobj, options, cb)
7284 {
7285 var log;
7286
7287 // options is optional
7288 if (arguments.length === 2) {
7289 cb = arguments[1];
7290 options = {};
7291 }
7292
7293 if (!vmobj || !vmobj.hasOwnProperty('uuid')
7294 || !vmobj.hasOwnProperty('zonename')) {
7295
7296 cb(new Error('markVMFailure needs uuid + zonename'));
7297 return;
7298 }
7299
7300 ensureLogging(true);
7301 if (options.hasOwnProperty('log')) {
7302 log = options.log;
7303 } else {
7304 log = VM.log.child({action: 'markVMFailure', vm: vmobj.uuid});
7305 }
7306
7307 function dumpDebugInfo(zonename, callback) {
7308 var errors = {};
7309
7310 async.series([
7311 function (ptree_cb) {
7312 // note: if the zone is not running this returns empty but still
7313 // exits 0
7314 execFile('/usr/bin/ptree', ['-z', zonename],
7315 function (ptree_err, ptree_stdout, ptree_stderr) {
7316
7317 if (ptree_err) {
7318 log.error(ptree_err, 'unable to get ptree from '
7319 + zonename + ': ' + ptree_stderr);
7320 errors.ptree_err = ptree_err;
7321 } else {
7322 log.warn('processes running in ' + zonename
7323 + ' at fail time:\n' + ptree_stdout);
7324 }
7325
7326 ptree_cb(); // don't fail on error here.
7327 }
7328 );
7329 }, function (svcs_cb) {
7330 execFile('/usr/bin/svcs', ['-xv', '-z', zonename],
7331 function (svcs_err, svcs_stdout, svcs_stderr) {
7332
7333 if (svcs_err) {
7334 log.error(svcs_err, 'unable to get svcs from '
7335 + zonename + ': ' + svcs_stderr);
7336 errors.svcs_err = svcs_err;
7337 } else {
7338 log.warn('svcs -xv output for ' + zonename
7339 + ' at fail time:\n' + svcs_stdout);
7340 }
7341
7342 svcs_cb(); // don't fail on error here.
7343 }
7344 );
7345 }, function (kstat_cb) {
7346 execFile('/usr/bin/kstat', ['-n', zonename.substr(0, 30)],
7347 function (kstat_err, kstat_stdout, kstat_stderr) {
7348
7349 if (kstat_err) {
7350 log.error(kstat_err, 'unable to get kstats from '
7351 + zonename + ': ' + kstat_stderr);
7352 errors.kstat_err = kstat_err;
7353 } else {
7354 log.warn('kstat output for ' + zonename
7355 + ' at fail time:\n' + kstat_stdout);
7356 }
7357
7358 kstat_cb(); // don't fail on error here.
7359 }
7360 );
7361 }
7362 ], function () {
7363 callback(errors);
7364 });
7365 }
7366
7367 dumpDebugInfo(vmobj.zonename, function (debug_err) {
7368 var zcfg;
7369
7370 // note: we don't treat failure to dump debug info as a fatal error.
7371 log.warn(debug_err, 'zone setup failed, zone is being stopped '
7372 + 'for manual investigation.');
7373
7374 // Mark the zone as 'failed'
7375 zcfg = 'remove -F attr name=failed; add attr; set name=failed; '
7376 + 'set value="provisioning"; set type=string; end';
7377
7378 zonecfg(['-u', vmobj.uuid, zcfg], log, function (zonecfg_err, fds) {
7379
7380 if (zonecfg_err) {
7381 log.error({err: zonecfg_err, stdout: fds.stdout,
7382 stderr: fds.stderr}, 'Unable to set failure flag on '
7383 + vmobj.uuid + ': ' + zonecfg_err.message);
7384 } else {
7385 log.debug({stdout: fds.stdout, stderr: fds.stderr},
7386 'set failure flag on ' + vmobj.uuid);
7387 }
7388
7389 // attempt to remove transition
7390 VM.unsetTransition(vmobj, {log: log}, function (unset_err) {
7391 if (unset_err) {
7392 log.error(unset_err);
7393 }
7394
7395 VM.stop(vmobj.uuid, {force: true, log: log},
7396 function (stop_err) {
7397
7398 // only log errors because there's nothing to do
7399
7400 if (stop_err) {
7401 log.error(stop_err, 'failed to stop VM '
7402 + vmobj.uuid + ': ' + stop_err.message);
7403 }
7404
7405 cb();
7406 });
7407 });
7408 });
7409 });
7410 };
7411
7412 function svccfg(zonepath, args, log, callback)
7413 {
7414 var cmd = '/usr/sbin/svccfg';
7415 var exec_options = {};
7416 var zoneroot = path.join(zonepath, '/root');
7417
7418 assert(log, 'no logger passed to svccfg()');
7419
7420 try {
7421 assertSafeZonePath(zoneroot, '/etc/svc/repository.db',
7422 {type: 'file', enoent_ok: false});
7423 } catch (e) {
7424 log.error(e, 'Error validating /etc/svc/repository.db: ' + e.message);
7425 callback(e);
7426 return;
7427 }
7428
7429 exec_options = {
7430 env: {
7431 'SVCCFG_CONFIGD_PATH': '/lib/svc/bin/svc.configd',
7432 'SVCCFG_REPOSITORY':
7433 path.join(zonepath, 'root', '/etc/svc/repository.db')
7434 }
7435 };
7436
7437 log.debug({'command': cmd + ' ' + args.join(' '),
7438 'exec_options': exec_options}, 'modifying svc repo in ' + zonepath);
7439 execFile(cmd, args, exec_options, function (error, stdout, stderr) {
7440 if (error) {
7441 callback(error, {'stdout': stdout, 'stderr': stderr});
7442 } else {
7443 callback(null, {'stdout': stdout, 'stderr': stderr});
7444 }
7445 });
7446 }
7447
7448 // This calls cb() when /var/svc/provisioning is gone. When this calls cb()
7449 // with an Error object, the provision is considered failed so this should
7450 // only happen when something timed out that is unrelated to the user.
7451 //
7452 // This returns a function that can be called with no arguments to cancel
7453 // all timers and actions pending from this function. It will also then not
7454 // call the cb().
7455 //
7456 // IMPORTANT: this is only exported to be used by vmadmd. Do not use elsewhere!
7457 //
7458 // vmobj fields:
7459 //
7460 // state
7461 // transition_expire
7462 // uuid
7463 // zonepath
7464 //
7465 exports.waitForProvisioning = function (vmobj, options, cb)
7466 {
7467 var dirname = path.join(vmobj.zonepath, 'root', '/var/svc');
7468 var filename = path.join(dirname, 'provisioning');
7469 var ival_h;
7470 var log;
7471 var timeout;
7472 var timeout_remaining = PROVISION_TIMEOUT; // default to whole thing
7473 var watcher;
7474
7475 // options is optional
7476 if (arguments.length === 2) {
7477 cb = arguments[1];
7478 options = {};
7479 }
7480
7481 ensureLogging(true);
7482 if (options.hasOwnProperty('log')) {
7483 log = options.log;
7484 } else {
7485 log = VM.log.child({action: 'waitForProvisioning', vm: vmobj.uuid});
7486 }
7487
7488 function done() {
7489 if (timeout) {
7490 log.debug('clearing provision timeout for ' + vmobj.uuid);
7491 clearTimeout(timeout);
7492 timeout = null;
7493 }
7494 if (watcher) {
7495 log.debug('closing /var/svc/provisioning watcher for '
7496 + vmobj.uuid);
7497 watcher.close();
7498 watcher = null;
7499 }
7500 if (ival_h) {
7501 log.debug('closing hwsetup check interval for ' + vmobj.uuid);
7502 clearInterval(ival_h);
7503 ival_h = null;
7504 }
7505 }
7506
7507 if ((vmobj.state === 'provisioning')
7508 && (vmobj.hasOwnProperty('transition_expire'))) {
7509
7510 timeout_remaining =
7511 (Number(vmobj.transition_expire) - Date.now(0)) / 1000;
7512
7513 // Always give it at least 1 second's chance.
7514 if (timeout_remaining < 1) {
7515 timeout_remaining = 1;
7516 }
7517 } else {
7518 // don't know what to do here we're not provisioning.
7519 log.warn('waitForProvisioning called when ' + vmobj.uuid
7520 + ' was not provisioning');
7521 cb();
7522 return (null);
7523 }
7524
7525 log.debug({
7526 'transition_expire': Number(vmobj.transition_expire),
7527 'now': Date.now(0)
7528 }, 'waiting ' + timeout_remaining + ' sec(s) for provisioning');
7529
7530 log.debug('setting provision timeout for ' + vmobj.uuid);
7531 timeout = setTimeout(function () {
7532 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure" because we '
7533 + 'hit waitForProvisioning() timeout.');
7534 VM.markVMFailure(vmobj, {log: log}, function (err) {
7535 var errstr = 'timed out waiting for /var/svc/provisioning to move'
7536 + ' for ' + vmobj.uuid;
7537 if (err) {
7538 log.warn(err, 'markVMFailure(): ' + err.message);
7539 }
7540 log.error(errstr);
7541 done();
7542 cb(new Error(errstr));
7543 });
7544 }, (timeout_remaining * 1000));
7545
7546 // this starts a loop that will move provisioning -> provision_success when
7547 // the hardware of the VM has been initialized the first time.
7548 if (BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) {
7549 ival_h = markProvisionedWhenHWSetup(vmobj, {log: log}, function (err) {
7550 if (err) {
7551 log.error(err, 'error in markProvisionedWhenHWSetup()');
7552 }
7553 done();
7554 cb(err);
7555 });
7556 return (done);
7557 }
7558
7559 watcher = fs.watch(filename, function (evt, file) {
7560 // We only care about 'rename' which also fires when the file is
7561 // deleted.
7562 log.debug('watcher.event(' + vmobj.uuid + '): ' + evt);
7563 if (evt === 'rename') {
7564 fs.exists(filename, function (exists) {
7565 if (exists) {
7566 // somehow we still have /var/svc/provisioning!
7567 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure"'
7568 + ' because we still have /var/svc/provisioning after '
7569 + 'rename');
7570 VM.markVMFailure(vmobj, {log: log}, function (err) {
7571 if (err) {
7572 log.warn(err, 'markVMFailure(): ' + err.message);
7573 }
7574 done();
7575 cb(new Error('/var/svc/provisioning exists after '
7576 + 'rename!'));
7577 });
7578 return;
7579 }
7580
7581 // So long as /var/svc/provisioning is gone, we don't care what
7582 // replaced it. Success or failure of user script doesn't
7583 // matter for the state, it's provisioned now. Caller should
7584 // now clear the transition.
7585 done();
7586 cb();
7587 return;
7588 });
7589 }
7590 });
7591
7592 log.debug('created watcher for ' + vmobj.uuid);
7593 return (done);
7594 };
7595
7596 // create and install a 'joyent' or 'kvm' brand zone.
7597 function installZone(payload, log, callback)
7598 {
7599 var load_fields;
7600 var receiving = false;
7601 var reprovisioning = false;
7602 var vmobj;
7603 var zoneinit = {};
7604
7605 assert(log, 'no logger passed to installZone()');
7606
7607 log.debug('installZone()');
7608
7609 load_fields = [
7610 'brand',
7611 'firewall_enabled',
7612 'missing',
7613 'nics',
7614 'owner_uuid',
7615 'routes',
7616 'state',
7617 'tags',
7618 'transition_to',
7619 'transition_expire',
7620 'uuid',
7621 'zonename',
7622 'zonepath'
7623 ];
7624
7625 if (payload.reprovisioning) {
7626 log.debug('installZone(): reprovisioning');
7627 reprovisioning = true;
7628 }
7629
7630 async.series([
7631 function (cb) {
7632
7633 VM.load(payload.uuid, {fields: load_fields, log: log},
7634 function (err, obj) {
7635
7636 if (err) {
7637 cb(err);
7638 return;
7639 }
7640 vmobj = obj;
7641 cb();
7642 });
7643 }, function (cb) {
7644 var thing;
7645 var missing = false;
7646 var msg;
7647 var things = ['datasets', 'filesystems', 'disks'];
7648
7649 if (vmobj.state === 'receiving') {
7650 receiving = true;
7651 msg = 'zone is still missing:';
7652 for (thing in things) {
7653 thing = things[thing];
7654 if (vmobj.missing[thing].length !== 0) {
7655 msg = msg + ' ' + vmobj.missing[thing].length + ' '
7656 + thing + ',';
7657 missing = true;
7658 }
7659 }
7660 msg = rtrim(msg, ',');
7661
7662 if (missing) {
7663 cb(new Error('Unable to complete install for '
7664 + vmobj.uuid + ' ' + msg));
7665 return;
7666 }
7667 }
7668 cb();
7669 }, function (cb) {
7670 // Install the zone.
7671 // This will create the dataset and mark the zone 'installed'.
7672 var args;
7673
7674 if (reprovisioning) {
7675 // reprovisioning we do *most* of install, but not this.
7676 cb();
7677 return;
7678 }
7679
7680 args = ['-z', vmobj.zonename, 'install', '-q',
7681 payload.quota.toString()];
7682
7683 // For both OS and KVM VMs you can pass an image_uuid at the
7684 // top-level. This will be your zone's root dataset. On KVM the user
7685 // is never exposed to this. It's used there for something like
7686 // SPICE.
7687 if (payload.hasOwnProperty('image_uuid')) {
7688 args.push('-t', payload.image_uuid, '-x', 'nodataset');
7689 }
7690
7691 zoneadm(args, log, function (err, fds) {
7692 if (err) {
7693 log.error({err: err, stdout: fds.stdout,
7694 stderr: fds.stderr}, 'zoneadm failed to install: '
7695 + err.message);
7696 cb(err);
7697 } else {
7698 log.debug({stdout: fds.stdout, stderr: fds.stderr},
7699 'zoneadm installed zone');
7700 cb();
7701 }
7702 });
7703 }, function (cb) {
7704 // Apply compression if set
7705 var args = [];
7706 if (payload.hasOwnProperty('zfs_root_compression')) {
7707 args = ['set', 'compression='
7708 + payload.zfs_root_compression, payload.zfs_filesystem];
7709 zfs(args, log, function (err) {
7710 cb(err);
7711 });
7712 } else {
7713 cb();
7714 }
7715 }, function (cb) {
7716 // Apply recsize if set
7717 var args = [];
7718 if (payload.hasOwnProperty('zfs_root_recsize')) {
7719 args = ['set', 'recsize=' + payload.zfs_root_recsize,
7720 payload.zfs_filesystem];
7721 zfs(args, log, function (err) {
7722 cb(err);
7723 });
7724 } else {
7725 cb();
7726 }
7727 }, function (cb) {
7728 // Some zones can have an additional 'data' dataset delegated to
7729 // them for use in the zone. This will set that up. If the option
7730 // is not set, the following does nothing.
7731 if (!receiving && !reprovisioning) {
7732 createDelegatedDataset(payload, log, function (err) {
7733 if (err) {
7734 cb(err);
7735 } else {
7736 cb();
7737 }
7738 });
7739 } else {
7740 cb();
7741 }
7742 }, function (cb) {
7743 // Write out the zone's metadata
7744 // Note: we don't do this when receiving because dataset will
7745 // already contain metadata and we don't want to wipe that out.
7746 if (!receiving && !reprovisioning) {
7747 saveMetadata(payload, log, function (err) {
7748 if (err) {
7749 log.error(err, 'unable to save metadata: '
7750 + err.message);
7751 cb(err);
7752 } else {
7753 cb();
7754 }
7755 });
7756 } else {
7757 cb();
7758 }
7759 }, function (cb) {
7760 // Write out the zone's routes
7761 // Note: we don't do this when receiving because dataset will
7762 // already contain routes and we don't want to wipe that out.
7763 if (!receiving && !reprovisioning) {
7764 saveRoutes(payload, log, function (err) {
7765 if (err) {
7766 log.error(err, 'unable to save routes: '
7767 + err.message);
7768 cb(err);
7769 } else {
7770 cb();
7771 }
7772 });
7773 } else {
7774 cb();
7775 }
7776 }, function (cb) {
7777 // if we were receiving, we're done receiving now
7778 if (receiving) {
7779 VM.unsetTransition(vmobj, {log: log}, cb);
7780 } else {
7781 cb();
7782 }
7783 }, function (cb) {
7784 // var zoneinit is in installZone() scope
7785
7786 // when receiving zoneinit is never run.
7787 if (receiving) {
7788 cb();
7789 return;
7790 }
7791
7792 getZoneinitJSON(vmobj.zonepath, log, function (zoneinit_err, data) {
7793
7794 if (zoneinit_err) {
7795 // NOTE: not existing is not going to give us a zoneinit_err
7796 log.warn(zoneinit_err, 'error in getZoneinitJSON');
7797 cb(zoneinit_err);
7798 return;
7799 }
7800
7801 if (data) {
7802 zoneinit = data;
7803 } else {
7804 zoneinit = {};
7805 }
7806
7807 cb();
7808 });
7809 }, function (cb) {
7810 // var_svc_provisioning is at installZone() scope
7811
7812 // If we're not receiving, we're provisioning a new VM and in that
7813 // case we write the /var/svc/provisioning file which should exist
7814 // until something in the zone decides provisioning is complete. At
7815 // that point it will be moved to either:
7816 //
7817 // /var/svc/provision_success
7818 // /var/svc/provision_failure
7819 //
7820 // to indicate that the provisioning setup has been completed.
7821
7822 if (receiving) {
7823 cb();
7824 return;
7825 }
7826
7827 fs.writeFile(path.join(vmobj.zonepath, 'root',
7828 '/var/svc/provisioning'), '', function (err, result) {
7829
7830 if (err) {
7831 log.error(err, 'failed to create '
7832 + '/var/svc/provisioning: ' + err.message);
7833 } else {
7834 log.debug('created /var/svc/provisioning in '
7835 + path.join(vmobj.zonepath, 'root'));
7836 }
7837
7838 cb(err);
7839 });
7840 }, function (cb) {
7841 // For joyent and joyent-minimal at least, set the timeout for the
7842 // svc start method to the value specified in the payload, or a
7843 // default.
7844
7845 var timeout;
7846
7847 if (BRAND_OPTIONS[vmobj.brand].features.update_mdata_exec_timeout) {
7848
7849 if (payload.hasOwnProperty('mdata_exec_timeout')) {
7850 timeout = payload.mdata_exec_timeout;
7851 } else {
7852 timeout = DEFAULT_MDATA_TIMEOUT;
7853 }
7854
7855 svccfg(vmobj.zonepath, [
7856 '-s', 'svc:/smartdc/mdata:execute',
7857 'setprop', 'start/timeout_seconds', '=', 'count:', timeout
7858 ], log, function (error, stdio) {
7859
7860 if (error) {
7861 log.error(error, 'failed to set mdata:exec timeout');
7862 cb(error);
7863 return;
7864 }
7865
7866 cb();
7867 });
7868 } else {
7869 cb();
7870 }
7871
7872 }, function (cb) {
7873 // This writes out the 'zoneconfig' file used by zoneinit to root's
7874 // home directory in the zone.
7875 if (! receiving
7876 && BRAND_OPTIONS[vmobj.brand].features.zoneinit
7877 && (! zoneinit.hasOwnProperty('features')
7878 || zoneinit.features.zoneconfig)) {
7879
7880 // No 'features' means old dataset. If we have old dataset or
7881 // one that really wants a zoneconfig, write it out.
7882
7883 writeZoneconfig(payload, log, function (err) {
7884 cb(err);
7885 });
7886 } else {
7887 cb();
7888 }
7889 }, function (cb) {
7890 if (BRAND_OPTIONS[vmobj.brand].features.write_zone_netfiles
7891 && !receiving) {
7892
7893 writeZoneNetfiles(payload, log, function (err) {
7894 cb(err);
7895 });
7896 } else {
7897 cb();
7898 }
7899 }, function (cb) {
7900 if (vmobj.hasOwnProperty('zonepath')
7901 && BRAND_OPTIONS[vmobj.brand].features.cleanup_dataset
7902 && !receiving) {
7903
7904 cleanupMessyDataset(vmobj.zonepath, vmobj.brand, log,
7905 function (err) {
7906
7907 cb(err);
7908 });
7909 } else {
7910 cb();
7911 }
7912 }, function (cb) {
7913 // Firewall data has not changed when reprovisioning, so we don't
7914 // re-run addFirewallData()
7915 if (reprovisioning) {
7916 cb();
7917 return;
7918 }
7919
7920 // Add firewall data if it was included
7921 addFirewallData(payload, vmobj, log, cb);
7922 }, function (cb) {
7923
7924 var cancel;
7925 var calledback = false;
7926 var prov_wait = true;
7927 // var_svc_provisioning is at installZone() scope
7928
7929 // The vm is now ready to start, we'll start if autoboot is set. If
7930 // not, we also don't want to wait for 'provisioning'.
7931 if (!payload.autoboot) {
7932 cb();
7933 return;
7934 }
7935
7936 // In these cases we never wait for provisioning -> running
7937 if (payload.nowait || receiving || vmobj.state !== 'provisioning') {
7938 prov_wait = false;
7939 }
7940
7941 // most VMs support the /var/svc/provision{ing,_success,_failure}
7942 // files. For those, if !nowait, we wait for the file to change
7943 // from provisioning -> either provision_success, or
7944 // provision_failure.
7945
7946 if (prov_wait) {
7947 // wait for /var/svc/provisioning -> provision_success/failure
7948 cancel = VM.waitForProvisioning(vmobj, {log: log},
7949 function (err) {
7950
7951 log.debug(err, 'waited for provisioning');
7952
7953 if (!err) {
7954 log.info('provisioning complete: '
7955 + '/var/svc/provisioning is gone');
7956 // this will clear the provision transition
7957 VM.unsetTransition(vmobj, {log: log},
7958 function (unset_err) {
7959
7960 if (unset_err) {
7961 log.error(unset_err, 'error unsetting '
7962 + 'transition: ' + unset_err.message);
7963 }
7964 // this and the cb in the VM.start callback might
7965 // both run if we don't check this.
7966 if (!calledback) {
7967 calledback = true;
7968 cb(unset_err);
7969 }
7970 });
7971 } else {
7972 // failed but might not be able to cb if VM.start's
7973 // callback already did.
7974 log.error(err, 'error waiting for provisioning: '
7975 + err.message);
7976 // this and the cb in the VM.start callback might
7977 // both run if we don't check this.
7978 if (!calledback) {
7979 calledback = true;
7980 cb(err);
7981 }
7982 }
7983 });
7984 }
7985
7986 VM.start(payload.uuid, {}, {log: log}, function (err, res) {
7987 if (err) {
7988 // we failed to start so we'll never see provisioning, so
7989 // cancel that and return the error.
7990 if (cancel) {
7991 log.info('cancelling VM.waitForProvisioning');
7992 cancel();
7993 }
7994 // this and the cb in the VM.waitForProvisioning
7995 // callback might both run if we don't check this.
7996 if (!calledback) {
7997 calledback = true;
7998 cb(err);
7999 }
8000 return;
8001 }
8002 // if we're waiting for 'provisioning' VM.waitForProvisioning's
8003 // callback will call cb(). If we're not going to wait, we call
8004 // it here.
8005 if (!prov_wait) {
8006 // this and the cb in the VM.waitForProvisioning
8007 // callback might both run if we don't check this.
8008 if (!calledback) {
8009 calledback = true;
8010 cb();
8011 }
8012 }
8013 });
8014 }], function (error) {
8015 callback(error);
8016 }
8017 );
8018 }
8019
8020 function getZoneinitJSON(rootpath, log, cb)
8021 {
8022 var filename;
8023 var zoneroot;
8024
8025 assert(log, 'no logger passed to getZoneinitJSON()');
8026
8027 zoneroot = path.join('/', rootpath, 'root');
8028 filename = path.join(zoneroot, '/var/zoneinit/zoneinit.json');
8029
8030 try {
8031 assertSafeZonePath(zoneroot, '/var/zoneinit/zoneinit.json',
8032 {type: 'file', enoent_ok: true});
8033 } catch (e) {
8034 log.error(e, 'Error validating /var/zoneinit/zoneinit.json: '
8035 + e.message);
8036 cb(e);
8037 return;
8038 }
8039
8040 fs.readFile(filename, function (error, data) {
8041 var zoneinit;
8042
8043 if (error && (error.code === 'ENOENT')) {
8044 // doesn't exist, leave empty
8045 log.debug('zoneinit.json does not exist.');
8046 cb();
8047 } else if (error) {
8048 // error reading: fail.
8049 cb(error);
8050 } else {
8051 // success try to load json
8052 try {
8053 zoneinit = JSON.parse(data.toString());
8054 log.debug({'zoneinit_json': zoneinit},
8055 'parsed zoneinit.json');
8056 cb(null, zoneinit);
8057 } catch (e) {
8058 cb(e);
8059 }
8060 }
8061 });
8062 }
8063
8064 function getDatasetMountpoint(dataset, log, callback)
8065 {
8066 var args;
8067 var cmd = '/usr/sbin/zfs';
8068 var mountpoint;
8069
8070 assert(log, 'no logger passed to getDatasetMountpoint()');
8071
8072 args = ['get', '-H', '-o', 'value', 'mountpoint', dataset];
8073
8074 log.debug(cmd + ' ' + args.join(' '));
8075 execFile(cmd, args, function (error, stdout, stderr) {
8076 if (error) {
8077 log.error(error, 'zfs get failed with: ' + stderr);
8078 callback(error);
8079 } else {
8080 mountpoint = stdout.replace(/\n/g, '');
8081 log.debug('mountpoint: "' + mountpoint + '"');
8082 callback(null, mountpoint);
8083 }
8084 });
8085 }
8086
8087 // TODO: pull data out of the massive zfs list we pulled earlier
8088 function checkDatasetProvisionable(payload, log, callback)
8089 {
8090 var dataset;
8091
8092 assert(log, 'no logger passed to checkDatasetProvisionable()');
8093
8094 if (BRAND_OPTIONS[payload.brand].features.var_svc_provisioning) {
8095 // when the brand always supports /var/svc/provisioning we don't have to
8096 // worry about the dataset not supporting it.
8097 callback(true);
8098 return;
8099 }
8100
8101 if (!payload.hasOwnProperty('zpool')
8102 || !payload.hasOwnProperty('image_uuid')) {
8103
8104 log.error('missing properties required to find dataset: '
8105 + JSON.stringify(payload));
8106 callback(false);
8107 return;
8108 }
8109
8110 dataset = payload.zpool + '/' + payload.image_uuid;
8111
8112 getDatasetMountpoint(dataset, log, function (dataset_err, mountpoint) {
8113 if (dataset_err) {
8114 log.error('unable to find mount point for ' + dataset);
8115 callback(false);
8116 return;
8117 }
8118
8119 getZoneinitJSON(dataset, log, function (zoneinit_err, zoneinit) {
8120 var filename_1_6_x;
8121 var filename_1_8_x;
8122
8123 if (zoneinit_err) {
8124 log.error(zoneinit_err, 'getZoneinitJSON() failed, assuming '
8125 + 'not provisionable.');
8126 callback(false);
8127 return;
8128 } else if (!zoneinit) {
8129 log.debug('no data from getZoneinitJSON(), using {}');
8130 zoneinit = {};
8131 }
8132
8133 if (zoneinit.hasOwnProperty('features')) {
8134 if (zoneinit.features.var_svc_provisioning) {
8135 log.info('zoneinit.features.var_svc_provisioning is '
8136 + 'set.');
8137 callback(true);
8138 return;
8139 }
8140 // we have features but not var_svc_provisioning === true means
8141 // we can't provision. Fall through and return false.
8142 } else {
8143 // Didn't load zoneinit features, so check for datasets that
8144 // have // 04-mdata.sh. For 1.6.x and earlier datasets this was
8145 // in /root but in 1.8.0 and 1.8.1 it is in /var/zoneinit. For
8146 // 1.8.2 and later we'll not get here as the zoneinit.json will
8147 // exist and we'll use that.
8148 filename_1_6_x = path.join(mountpoint, 'root',
8149 '/root/zoneinit.d/04-mdata.sh');
8150 filename_1_8_x = path.join(mountpoint, 'root',
8151 '/var/zoneinit/includes/04-mdata.sh');
8152
8153 if (fs.existsSync(filename_1_6_x)) {
8154 log.info(filename_1_6_x + ' exists');
8155 callback(true);
8156 return;
8157 } else {
8158 log.debug(filename_1_6_x + ' does not exist');
8159 if (fs.existsSync(filename_1_8_x)) {
8160 log.info(filename_1_8_x + ' exists');
8161 callback(true);
8162 return;
8163 } else {
8164 log.debug(filename_1_8_x + ' does not exist');
8165 // this was our last chance.
8166 // Fall through and return false.
8167 }
8168 }
8169 }
8170
8171 callback(false);
8172 return;
8173 });
8174 });
8175 }
8176
8177 // create and install a 'joyent' or 'kvm' brand zone.
8178 function createZone(payload, log, callback)
8179 {
8180 var create_time;
8181 var n;
8182 var now = new Date;
8183 var primary_found;
8184 var provision_timeout = PROVISION_TIMEOUT;
8185 var t;
8186 var vm_version;
8187 var zcfg;
8188
8189 assert(log, 'no logger passed to createZone()');
8190
8191 log.debug('createZone()');
8192
8193 payload.zfs_filesystem = payload.zpool + '/' + payload.zonename;
8194 payload.zonepath = '/' + payload.zfs_filesystem;
8195
8196 // we add create-timestamp in all cases except where we're receiving since
8197 // in that case we want to preserve the original create-timestamp.
8198 if (!payload.hasOwnProperty('transition')
8199 || (payload.transition.transition !== 'receiving')
8200 || !payload.hasOwnProperty('create_timestamp')) {
8201
8202 create_time = now.toISOString();
8203 } else {
8204 create_time = payload.create_timestamp;
8205 }
8206
8207 // we add vm-version (property v) in all cases except where we're receiving
8208 // since in that case we want to preserve the original version.
8209 if (!payload.hasOwnProperty('transition')
8210 || (payload.transition.transition !== 'receiving')
8211 || !payload.hasOwnProperty('v')) {
8212
8213 vm_version = 1;
8214 } else {
8215 vm_version = payload.v;
8216 }
8217
8218 // set the properties that can't be updated later here.
8219 zcfg = 'create -b\n'
8220 + 'set zonepath=' + payload.zonepath + '\n'
8221 + 'set brand=' + payload.brand + '\n'
8222 + 'set uuid=' + payload.uuid + '\n'
8223 + 'set ip-type=exclusive\n'
8224 + 'add attr; set name="vm-version"; set type=string; set value="'
8225 + vm_version + '"; end\n'
8226 + 'add attr; set name="create-timestamp"; set type=string; set value="'
8227 + create_time + '"; end\n';
8228
8229 if (payload.hasOwnProperty('transition')) {
8230 // IMPORTANT: this is for internal use only and should not be documented
8231 // as an option for create's payload. Used for receive.
8232 t = payload.transition;
8233 zcfg = zcfg
8234 + buildTransitionZonecfg(t.transition, t.target, t.timeout) + '\n';
8235 } else {
8236 // Assume this is really a new VM, add transition called 'provisioning'
8237 // only if the machine is going to be booting.
8238 if (!payload.hasOwnProperty('autoboot') || payload.autoboot) {
8239 zcfg = zcfg + buildTransitionZonecfg('provisioning', 'running',
8240 provision_timeout * 1000) + '\n';
8241 }
8242 }
8243
8244 // We call the property 'dataset-uuid' even though the property name is
8245 // image_uuid because existing VMs in the wild will be using dataset-uuid
8246 // already, and we are the point where the image becomes a dataset anyway.
8247 if (payload.hasOwnProperty('image_uuid')) {
8248 zcfg = zcfg + 'add attr; set name="dataset-uuid"; set type=string; '
8249 + 'set value="' + payload.image_uuid + '"; end\n';
8250 }
8251
8252 if (BRAND_OPTIONS[payload.brand].features.use_vm_autoboot) {
8253 // we always set autoboot=false for VM zones, since we want vmadmd to
8254 // boot them and not the zones tools. Use vm-autoboot to control VMs
8255 zcfg = zcfg + 'set autoboot=false\n';
8256 }
8257
8258 // ensure that we have a primary nic, even if one wasn't specified
8259 if (payload.hasOwnProperty('add_nics') && payload.add_nics.length != 0) {
8260 primary_found = false;
8261
8262 for (n in payload.add_nics) {
8263 n = payload.add_nics[n];
8264 if (n.hasOwnProperty('primary') && n.primary) {
8265 primary_found = true;
8266 break;
8267 }
8268 }
8269 if (!primary_found) {
8270 payload.add_nics[0].primary = true;
8271 }
8272 }
8273
8274 // Passing an empty first parameter here, tells buildZonecfgUpdate that
8275 // we're talking about a new machine.
8276 zcfg = zcfg + buildZonecfgUpdate({}, payload, log);
8277
8278 // include the zonecfg in the debug output to help track down problems.
8279 log.debug(zcfg);
8280
8281 // send the zonecfg data we just generated as a file to zonecfg,
8282 // this will create the zone.
8283 zonecfgFile(zcfg, ['-z', payload.zonename], log, function (err, fds) {
8284 if (err || payload.create_only) {
8285 log.error({err: err, zcfg: zcfg, stdout: fds.stdout,
8286 stderr: fds.stderr}, 'failed to modify zonecfg');
8287 callback(err);
8288 } else {
8289 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8290 'modified zonecfg');
8291 installZone(payload, log, callback);
8292 }
8293 });
8294 }
8295
8296 function normalizeNics(payload, vmobj)
8297 {
8298 var n;
8299 var nic;
8300
8301 // ensure all NICs being created/added have a MAC, remove the 'index' if it
8302 // is passed (that's deprecated), rename 'interface' to 'physical'.
8303 if (payload.hasOwnProperty('add_nics')) {
8304 for (n in payload.add_nics) {
8305 if (payload.add_nics.hasOwnProperty(n)) {
8306 nic = payload.add_nics[n];
8307
8308 if (!nic.hasOwnProperty('mac')
8309 && !nic.hasOwnProperty('vrrp_vrid')) {
8310 nic.mac = generateMAC();
8311 }
8312 delete nic.index;
8313 if (nic.hasOwnProperty('interface')) {
8314 nic.physical = nic.interface;
8315 delete nic.interface;
8316 }
8317
8318 // nics.*.primary only supports true value, unset false. We also
8319 // handle the case here why they used the deprecated '1' value.
8320 // We will have already warned them, but still support for now.
8321 if (nic.hasOwnProperty('primary')) {
8322 if (nic.primary || nic.primary === '1'
8323 || nic.primary === 1) {
8324
8325 nic.primary = true;
8326 } else {
8327 delete nic.primary;
8328 }
8329 }
8330 }
8331 }
8332 }
8333 }
8334
8335 /*
8336 * This is called for both create and update, everything here should be safe for
8337 * both. The vmobj will be set if it's an update.
8338 *
8339 */
8340 function normalizePayload(payload, vmobj, log, callback)
8341 {
8342 var action;
8343 var allowed;
8344 var brand;
8345 var property;
8346
8347 assert(log, 'no logger passed to normalizePayload()');
8348
8349 // fix type of arguments that should be numbers, do this here so that fixing
8350 // memory works correctly later using math.
8351 for (property in payload) {
8352 if (payload.hasOwnProperty(property)) {
8353 if (PAYLOAD_PROPERTIES.hasOwnProperty(property)
8354 && PAYLOAD_PROPERTIES[property].type === 'integer'
8355 && payload[property] !== undefined) {
8356 // undefined is a special case since we use that to unset props
8357
8358 payload[property] = Number(payload[property]);
8359 if (isNaN(payload[property])) {
8360 callback(new Error('Invalid value for ' + property + ': '
8361 + JSON.stringify(payload[property]) + ':'
8362 + typeof (payload[property])));
8363 return;
8364 }
8365 }
8366 }
8367 }
8368
8369 if (payload.hasOwnProperty('quota') && payload.quota === undefined) {
8370 // when unsetting quota we set to 0
8371 payload.quota = 0;
8372 }
8373
8374 if (vmobj) {
8375 /* update */
8376 fixPayloadMemory(payload, vmobj, log);
8377 action = 'update';
8378 } else {
8379 /* this also calls fixPayloadMemory() */
8380 applyZoneDefaults(payload, log);
8381
8382 if (payload.hasOwnProperty('create_only')
8383 && payload.transition.transition === 'receiving') {
8384
8385 action = 'receive';
8386 } else {
8387 action = 'create';
8388 }
8389 }
8390
8391 // Should always have a brand after we applied defaults.
8392 if (vmobj && vmobj.hasOwnProperty('brand')) {
8393 brand = vmobj.brand;
8394 } else if (payload.hasOwnProperty('brand')) {
8395 brand = payload.brand;
8396 } else {
8397 callback(new Error('Unable to determine brand for payload'));
8398 return;
8399 }
8400
8401 // Historically we supported dataset_uuid for joyent+joyent-minimal and
8402 // zone_dataset_uuid for kvm. Now we just support image_uuid so give a
8403 // deprecation warning and translate if old version specified. This needs
8404 // to happen before VM.validate because image_uuid is required for most
8405 // VMs.
8406 allowed = BRAND_OPTIONS[brand].allowed_properties;
8407 if ((allowed.hasOwnProperty('dataset_uuid')
8408 && payload.hasOwnProperty('dataset_uuid'))
8409 || (allowed.hasOwnProperty('zone_dataset_uuid')
8410 && payload.hasOwnProperty('zone_dataset_uuid'))) {
8411
8412 property = (payload.hasOwnProperty('dataset_uuid') ? 'dataset_uuid'
8413 : 'zone_dataset_uuid');
8414
8415 if (payload.hasOwnProperty('image_uuid')) {
8416 log.warn('DEPRECATED option ' + property + ' found, '
8417 + 'ignoring. In the future use image_uuid only.');
8418 } else {
8419 log.warn('DEPRECATED option ' + property + ' found, '
8420 + 'ignoring. In the future use image_uuid instead.');
8421 payload.image_uuid = payload[property];
8422 delete payload.dataset_uuid;
8423 }
8424 }
8425
8426 // after ZoneDefaults have been applied, we should always have zone. Now
8427 // we validate the payload properties and remove any that are invalid. If
8428 // there are bad values we'll just fail.
8429 VM.validate(brand, action, payload, {log: log}, function (errors) {
8430 var bad_prop;
8431 var compound_props = ['disks', 'nics', 'filesystems'];
8432 var matches;
8433 var obj;
8434 var prop;
8435
8436 if (errors) {
8437 if (errors.hasOwnProperty('bad_brand')) {
8438 callback(new Error('Invalid brand while validating payload: '
8439 + JSON.stringify(brand)));
8440 return;
8441 }
8442 if (errors.bad_values.length > 0) {
8443 callback(new Error('Invalid value(s) for: '
8444 + errors.bad_values.join(',')));
8445 return;
8446 }
8447 if (errors.missing_properties.length > 0) {
8448 callback(new Error('Missing required properties: '
8449 + errors.missing_properties.join(',')));
8450 return;
8451 }
8452 for (bad_prop in errors.bad_properties) {
8453 bad_prop = errors.bad_properties[bad_prop];
8454 log.warn('Warning, invalid ' + action + ' property: ['
8455 + bad_prop + '] removing from payload.');
8456
8457 // for bad properties like nics.*.allow_unfiltered_promisc we
8458 // need to remove it from add_nics, update_nics, etc.
8459 for (prop in compound_props) {
8460 prop = compound_props[prop];
8461
8462 matches = new RegExp('^' + prop
8463 + '\\.\\*\\.(.*)$').exec(bad_prop);
8464 if (matches) {
8465 if (payload.hasOwnProperty(prop)) {
8466 for (obj in payload[prop]) {
8467 delete payload[prop][obj][matches[1]];
8468 }
8469 }
8470 if (payload.hasOwnProperty('add_' + prop)) {
8471 for (obj in payload['add_' + prop]) {
8472 delete payload['add_' + prop][obj][matches[1]];
8473 }
8474 }
8475 if (payload.hasOwnProperty('update_' + prop)) {
8476 for (obj in payload['update_' + prop]) {
8477 delete payload['update_'
8478 + prop][obj][matches[1]];
8479 }
8480 }
8481 }
8482 }
8483
8484 delete payload[bad_prop];
8485 }
8486 }
8487
8488 // By the time we got here all the properties in the payload are allowed
8489
8490 // Now we make sure we've got a zonename (use uuid if not already set)
8491 if (!payload.hasOwnProperty('zonename')
8492 || payload.zonename === undefined) {
8493
8494 payload.zonename = payload.uuid;
8495 }
8496
8497 // You use 'disks' and 'nics' when creating, but the underlying
8498 // functions expect add_disks and add_nics, so we rename them now that
8499 // we've confirmed we've got the correct thing for this action.
8500 if (payload.hasOwnProperty('disks')) {
8501 if (payload.hasOwnProperty('add_disks')) {
8502 callback(new Error('Cannot specify both "disks" and '
8503 + '"add_disks"'));
8504 return;
8505 }
8506 payload.add_disks = payload.disks;
8507 delete payload.disks;
8508 }
8509 if (payload.hasOwnProperty('nics')) {
8510 if (payload.hasOwnProperty('add_nics')) {
8511 callback(new Error('Cannot specify both "nics" and '
8512 + '"add_nics"'));
8513 return;
8514 }
8515 payload.add_nics = payload.nics;
8516 delete payload.nics;
8517 }
8518 if (payload.hasOwnProperty('filesystems')) {
8519 if (payload.hasOwnProperty('add_filesystems')) {
8520 callback(new Error('Cannot specify both "filesystems" and '
8521 + '"add_filesystems"'));
8522 return;
8523 }
8524 payload.add_filesystems = payload.filesystems;
8525 delete payload.filesystems;
8526 }
8527
8528 // if there's a zfs_root_* and no zfs_data_*, normally the properties
8529 // would fall through, we don't want that.
8530 if (payload.hasOwnProperty('zfs_root_compression')
8531 && !payload.hasOwnProperty('zfs_data_compression')) {
8532
8533 if (vmobj && vmobj.hasOwnProperty('zfs_data_compression')) {
8534 // keep existing value.
8535 payload.zfs_data_compression = vmobj.zfs_data_compression;
8536 } else {
8537 // keep default value.
8538 payload.zfs_data_compression = 'off';
8539 }
8540 }
8541 if (payload.hasOwnProperty('zfs_root_recsize')
8542 && !payload.hasOwnProperty('zfs_data_recsize')) {
8543
8544 if (vmobj && vmobj.hasOwnProperty('zfs_data_recsize')) {
8545 // keep existing value.
8546 payload.zfs_data_recsize = vmobj.zfs_data_recsize;
8547 } else {
8548 // keep default value.
8549 payload.zfs_data_recsize = 131072;
8550 }
8551 }
8552
8553 // this will ensure we've got a MAC, etc.
8554 normalizeNics(payload, vmobj);
8555
8556 // Fix types for boolean fields in case someone put in 'false'/'true'
8557 // instead of false/true
8558 for (property in payload) {
8559 if (payload.hasOwnProperty(property)) {
8560 if (PAYLOAD_PROPERTIES.hasOwnProperty(property)
8561 && PAYLOAD_PROPERTIES[property].type === 'boolean') {
8562
8563 payload[property] = fixBooleanLoose(payload[property]);
8564 }
8565 }
8566 }
8567
8568 // We used to support zfs_storage_pool_name, but zpool is better.
8569 if (payload.hasOwnProperty('zfs_storage_pool_name')) {
8570 if (payload.hasOwnProperty('zpool')) {
8571 log.warn('DEPRECATED option zfs_storage_pool_name found, '
8572 + 'ignoring!');
8573 } else {
8574 log.warn('DEPRECATED option zfs_storage_pool_name found, '
8575 + 'replacing with zpool!');
8576 payload.zpool = payload.zfs_storage_pool_name;
8577 delete payload.zfs_storage_pool_name;
8578 }
8579 }
8580
8581 // When creating a VM with SPICE you need the image_uuid, if you don't
8582 // pass that, we'll remove any SPICE options.
8583 if (action === 'create'
8584 && !payload.hasOwnProperty('image_uuid')) {
8585
8586 if (payload.hasOwnProperty('spice_opts')
8587 || payload.hasOwnProperty('spice_password')
8588 || payload.hasOwnProperty('spice_port')) {
8589
8590 log.warn('Creating with SPICE options requires '
8591 + 'image_uuid, REMOVING spice_*');
8592 delete payload.spice_opts;
8593 delete payload.spice_password;
8594 delete payload.spice_port;
8595 }
8596 }
8597
8598 checkPayloadProperties(payload, vmobj, log, function (e) {
8599 if (e) {
8600 callback(e);
8601 } else {
8602 callback();
8603 }
8604 });
8605 });
8606 }
8607
8608 function buildTransitionZonecfg(transition, target, timeout)
8609 {
8610 var cmdline;
8611
8612 cmdline = 'add attr; set name=transition; set value="'
8613 + transition + ':' + target + ':' + (Date.now(0) + timeout).toString()
8614 + '"; set type=string; end';
8615
8616 return cmdline;
8617 }
8618
8619 // vmobj should have:
8620 //
8621 // uuid
8622 // transition_to (if set)
8623 //
8624 exports.unsetTransition = function (vmobj, options, callback)
8625 {
8626 var log;
8627
8628 // options is optional
8629 if (arguments.length === 2) {
8630 callback = arguments[1];
8631 options = {};
8632 }
8633
8634 ensureLogging(true);
8635 if (options.hasOwnProperty('log')) {
8636 log = options.log;
8637 } else {
8638 log = VM.log.child({action: 'unsetTransition', vm: vmobj.uuid});
8639 }
8640
8641 zonecfg(['-u', vmobj.uuid, 'remove -F attr name=transition'], log,
8642 function (err, fds) {
8643
8644 if (err) {
8645 // log at info because this might be because already removed
8646 log.info({err: err, stdout: fds.stdout, stderr: fds.stderr},
8647 'unable to remove transition for zone ' + vmobj.uuid);
8648 } else {
8649 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8650 'removed transition for zone ' + vmobj.uuid);
8651 }
8652
8653 zonecfg(['-u', vmobj.uuid, 'info attr name=transition'], log,
8654 function (info_err, info_fds) {
8655
8656 if (info_err) {
8657 log.error({err: info_err, stdout: info_fds.stdout,
8658 stderr: info_fds.stderr},
8659 'failed to confirm transition removal');
8660 callback(info_err);
8661 return;
8662 }
8663
8664 if (info_fds.stdout !== 'No such attr resource.\n') {
8665 log.error({stdout: info_fds.stdout, stderr: info_fds.stderr},
8666 'unknown error checking transition after removal');
8667 callback(new Error('transition does not appear to have been '
8668 + 'removed zonecfg said: ' + JSON.stringify(info_fds)));
8669 return;
8670 }
8671
8672 // removed the transition, now attempt to start if we're rebooting.
8673 if (vmobj.transition_to && vmobj.transition_to === 'start') {
8674 log.debug('VM ' + vmobj.uuid + ' was stopping for reboot, '
8675 + 'transitioning to start.');
8676 VM.start(vmobj.uuid, {}, {log: log}, function (e) {
8677 if (e) {
8678 log.error(e, 'failed to start when clearing '
8679 + 'transition');
8680 }
8681 callback();
8682 });
8683 } else {
8684 callback();
8685 }
8686 });
8687 });
8688 };
8689
8690 //
8691 // vmobj fields used:
8692 //
8693 // transition
8694 // uuid
8695 //
8696 function setTransition(vmobj, transition, target, timeout, log, callback)
8697 {
8698 assert(log, 'no logger passed to setTransition()');
8699
8700 if (!timeout) {
8701 callback(new Error('setTransition() requires timeout argument.'));
8702 return;
8703 }
8704
8705 async.series([
8706 function (cb) {
8707 // unset an existing transition
8708 if (vmobj.hasOwnProperty('transition')) {
8709 VM.unsetTransition(vmobj, {log: log}, cb);
8710 } else {
8711 cb();
8712 }
8713 }, function (cb) {
8714 var zcfg;
8715
8716 zcfg = buildTransitionZonecfg(transition, target, timeout);
8717 zonecfg(['-u', vmobj.uuid, zcfg], log, function (err, fds) {
8718 if (err) {
8719 log.error({err: err, stdout: fds.stdout,
8720 stderr: fds.stderr}, 'failed to set transition='
8721 + transition + ' for VM ' + vmobj.uuid);
8722 } else {
8723 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8724 'set transition=' + transition + ' for vm '
8725 + vmobj.uuid);
8726 }
8727
8728 cb(err);
8729 });
8730 }
8731 ], function (error) {
8732 callback(error);
8733 });
8734 }
8735
8736 function receiveVM(json, log, callback)
8737 {
8738 var payload = {};
8739
8740 assert(log, 'no logger passed to receiveVM()');
8741
8742 try {
8743 payload = JSON.parse(json);
8744 } catch (e) {
8745 callback(e);
8746 return;
8747 }
8748
8749 payload.create_only = true;
8750
8751 // adding transition here is considered to be *internal only* not for
8752 // consumer use and not to be documented as a property you can use with
8753 // create.
8754 payload.transition =
8755 {'transition': 'receiving', 'target': 'stopped', 'timeout': 86400};
8756
8757 // We delete tags and metadata here becasue this exists in the root
8758 // dataset which we will be copying, so it would be duplicated here.
8759 delete payload.customer_metadata;
8760 delete payload.internal_metadata;
8761 delete payload.tags;
8762
8763 // On receive we need to make sure that we don't create new disks so we
8764 // mark them all as nocreate. We also can't set the block_size of imported
8765 // volumes, so we remove that.
8766 if (payload.hasOwnProperty('disks')) {
8767 var disk_idx;
8768
8769 for (disk_idx in payload.disks) {
8770 payload.disks[disk_idx].nocreate = true;
8771
8772 if (payload.disks[disk_idx].image_uuid) {
8773 delete payload.disks[disk_idx].block_size;
8774 }
8775 }
8776 }
8777
8778 VM.create(payload, {log: log}, function (err, result) {
8779 if (err) {
8780 callback(err);
8781 }
8782
8783 // don't include the special transition in the payload we write out.
8784 delete payload.transition;
8785
8786 fs.writeFile('/etc/zones/' + payload.uuid + '-receiving.json',
8787 JSON.stringify(payload, null, 2), function (e) {
8788
8789 if (e) {
8790 callback(e);
8791 return;
8792 }
8793
8794 // ready for datasets
8795 callback(null, result);
8796 });
8797 });
8798 }
8799
8800 function receiveStdinChunk(type, log, callback)
8801 {
8802 var child;
8803 var chunk_name = '';
8804 var chunk_size = 0;
8805 var json = '';
8806 var remaining = '';
8807
8808 assert(log, 'no logger passed to receiveStdinChunk()');
8809
8810 /*
8811 * XXX
8812 *
8813 * node 0.6.x removed support for arbitrary file descriptors which
8814 * means we can only handle stdin for now since we need to pass this
8815 * descriptor directly to the child. 0.8.x is supposed to reintroduce
8816 * this functionality. When we do, this should be changed to open
8817 * the file and set fd to the descriptor, and we should be able to
8818 * get rid of vmunbundle.
8819 *
8820 */
8821
8822 if (type === 'JSON') {
8823 log.info('/usr/vm/sbin/vmunbundle json');
8824 child = spawn('/usr/vm/sbin/vmunbundle', ['json'],
8825 {customFds: [0, -1, -1]});
8826 } else if (type === 'DATASET') {
8827 log.info('/usr/vm/sbin/vmunbundle dataset');
8828 child = spawn('/usr/vm/sbin/vmunbundle', ['dataset'],
8829 {customFds: [0, -1, -1]});
8830 } else {
8831 callback(new Error('Unsupported chunk type ' + type));
8832 }
8833
8834 child.stderr.on('data', function (data) {
8835 var idx;
8836 var line;
8837 var matches;
8838
8839 remaining += data.toString();
8840
8841 idx = remaining.indexOf('\n');
8842 while (idx > -1) {
8843 line = trim(remaining.substring(0, idx));
8844 remaining = remaining.substring(idx + 1);
8845
8846 log.debug('VMUNBUNDLE: ' + line);
8847 matches = line.match(/Size: ([\d]+)/);
8848 if (matches) {
8849 chunk_size = Number(matches[1]);
8850 }
8851 matches = line.match(/Name: \[(.*)\]/);
8852 if (matches) {
8853 chunk_name = matches[1];
8854 }
8855
8856 idx = remaining.indexOf('\n');
8857 }
8858 });
8859
8860 child.stdout.on('data', function (data) {
8861 json += data.toString();
8862 log.debug('json size is ' + json.length);
8863 });
8864
8865 child.on('close', function (code) {
8866 log.debug('vmunbundle process exited with code ' + code);
8867 if (code === 3) {
8868 log.debug('vmbundle: end of bundle.');
8869 callback(null, 'EOF');
8870 return;
8871 } else if (code !== 0) {
8872 callback(new Error('vmunbundle exited with code ' + code));
8873 return;
8874 }
8875
8876 // if it was a dataset, we've now imported it.
8877 // if it was json, we've now got it in the json var.
8878
8879 if (type === 'DATASET') {
8880 log.info('Imported dataset ' + chunk_name);
8881 // delete 'sending' snapshot
8882 zfs(['destroy', '-F', chunk_name + '@sending'], log,
8883 function (err, fds) {
8884 if (err) {
8885 log.warn(err, 'Failed to destroy ' + chunk_name
8886 + '@sending: ' + err.message);
8887 }
8888 callback();
8889 }
8890 );
8891 } else if (type === 'JSON' && chunk_name === 'JSON'
8892 && json.length <= chunk_size && json.length > 0) {
8893
8894 receiveVM(json, log, function (e, result) {
8895 if (e) {
8896 callback(e);
8897 return;
8898 }
8899 log.info('Receive returning: ' + JSON.stringify(result));
8900 callback(null, result);
8901 });
8902 } else {
8903 log.debug('type: [' + type + ']');
8904 log.debug('chunk_name: [' + chunk_name + ']');
8905 log.debug('chunk_size: [' + chunk_size + ']');
8906 log.debug('json.length: [' + json.length + ']');
8907 log.warn('Failed to get ' + type + '!');
8908 callback(new Error('Failed to get ' + type + '!'));
8909 }
8910 });
8911 }
8912
8913 exports.receive = function (target, options, callback)
8914 {
8915 var log;
8916
8917 // options is optional
8918 if (arguments.length === 2) {
8919 callback = arguments[1];
8920 options = {};
8921 }
8922
8923 ensureLogging(true);
8924
8925 // We don't know anything about this VM yet, so we don't create a
8926 // VM.log.child.
8927 if (options.hasOwnProperty('log')) {
8928 log = options.log;
8929 } else {
8930 log = VM.log;
8931 }
8932
8933 log.info('Receiving VM from: ' + JSON.stringify(target));
8934
8935 if (target.hasOwnProperty('host') && target.hasOwnProperty('port')) {
8936 // network receive not yet supported either.
8937 callback(new Error('cannot receive from ' + JSON.stringify(target)));
8938 return;
8939 } else if (typeof (target) !== 'string' || target !== '-') {
8940 callback(new Error('cannot receive from ' + JSON.stringify(target)));
8941 return;
8942 }
8943
8944 receiveStdinChunk('JSON', log, function (error, result) {
8945 var eof = false;
8946
8947 if (error) {
8948 callback(error);
8949 return;
8950 }
8951 if (result && result === 'EOF') {
8952 callback(new Error('unable to find JSON in stdin.'));
8953 } else if (result && result.hasOwnProperty('uuid')) {
8954 // VM started receive, now need datasets
8955
8956 // We have JSON, so we can log better now if we need one
8957 if (!options.hasOwnProperty('log')) {
8958 log = VM.log.child({action: 'receive', vm: result.uuid});
8959 }
8960
8961 log.info('Receiving VM ' + result.uuid);
8962 log.debug('now looking for datasets');
8963
8964 async.whilst(
8965 function () { return !eof; },
8966 function (cb) {
8967 receiveStdinChunk('DATASET', log, function (err, res) {
8968 if (err) {
8969 cb(err);
8970 return;
8971 }
8972 if (res === 'EOF') {
8973 eof = true;
8974 }
8975 cb();
8976 });
8977 }, function (err) {
8978 if (err) {
8979 callback(err);
8980 return;
8981 }
8982 // no error so we read all the datasets, try an install.
8983 log.info('receive calling VM.install: ' + eof);
8984 VM.install(result.uuid, {log: log}, function (e) {
8985 if (e) {
8986 log.warn(e, 'couldn\'t install VM: '
8987 + e.message);
8988 }
8989 callback(e, result);
8990 });
8991 }
8992 );
8993 } else {
8994 callback(new Error('unable to receive JSON'));
8995 }
8996 });
8997 };
8998
8999 exports.reprovision = function (uuid, payload, options, callback)
9000 {
9001 var log;
9002 var provision_timeout = PROVISION_TIMEOUT;
9003 var set_transition = false;
9004 var snapshot;
9005 var vmobj;
9006
9007 // options is optional
9008 if (arguments.length === 3) {
9009 callback = arguments[2];
9010 options = {};
9011 }
9012
9013 ensureLogging(true);
9014 if (options.hasOwnProperty('log')) {
9015 log = options.log;
9016 } else {
9017 log = VM.log.child({action: 'reprovision', vm: uuid});
9018 }
9019
9020 log.info('Reprovisioning VM ' + uuid + ', original payload:\n'
9021 + JSON.stringify(payload, null, 2));
9022
9023 async.waterfall([
9024 function (cb) {
9025 VM.load(uuid, {
9026 fields: [
9027 'brand',
9028 'datasets',
9029 'hostname',
9030 'nics',
9031 'quota',
9032 'state',
9033 'uuid',
9034 'zfs_filesystem',
9035 'zone_state',
9036 'zonename',
9037 'zonepath',
9038 'zpool'
9039 ],
9040 log: log
9041 }, function (err, obj) {
9042 if (err) {
9043 cb(err);
9044 return;
9045 }
9046 vmobj = obj;
9047 log.debug('Loaded VM is: ' + JSON.stringify(vmobj, null, 2));
9048 cb();
9049 });
9050 }, function (cb) {
9051 if (BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
9052 && BRAND_OPTIONS[vmobj.brand].features.reprovision
9053 && BRAND_OPTIONS[vmobj.brand].features.brand_install_script) {
9054
9055 cb();
9056 } else {
9057 cb(new Error('brand "' + vmobj.brand + '" does not yet support'
9058 + ' reprovision'));
9059 }
9060 }, function (cb) {
9061 // only support image_uuid at top level (for non-KVM currently)
9062 if (!payload.hasOwnProperty('image_uuid')) {
9063 cb(new Error('payload is missing image_uuid'));
9064 } else {
9065 cb();
9066 }
9067 }, function (cb) {
9068 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 1) {
9069 cb(new Error('cannot support reprovision with multiple '
9070 + 'delegated datasets'));
9071 return;
9072 } else if (vmobj.hasOwnProperty('datasets')
9073 && vmobj.datasets.length === 1
9074 && vmobj.datasets[0] !== vmobj.zfs_filesystem + '/data') {
9075
9076 cb(new Error('cannot support reprovision with non-standard "'
9077 + vmobj.datasets[0] + '" dataset'));
9078 return;
9079 }
9080 cb();
9081 }, function (cb) {
9082 // TODO: change here when we support zvols/KVM, add size
9083 // & change type
9084
9085 validateImage({
9086 type: 'zone-dataset',
9087 uuid: payload.image_uuid,
9088 zpool: vmobj.zpool
9089 }, log, function (e) {
9090 cb(e);
9091 });
9092 }, function (cb) {
9093 // ensure we're stopped before reprovision starts
9094 if (vmobj.zone_state !== 'installed') {
9095 VM.stop(uuid, {log: log}, function (e) {
9096 if (e) {
9097 log.error(e, 'unable to stop VM ' + uuid + ': '
9098 + e.message);
9099 }
9100 cb(e);
9101 });
9102 } else {
9103 cb();
9104 }
9105 }, function (cb) {
9106 // Set transition to provisioning now, we're going for it.
9107 setTransition(vmobj, 'provisioning', 'running',
9108 (provision_timeout * 1000), log, function (err) {
9109 if (err) {
9110 cb(err);
9111 } else {
9112 set_transition = true;
9113 cb();
9114 }
9115 });
9116 }, function (cb) {
9117 // we validated any delegated dataset above, so we just need to
9118 // remove the 'zoned' flag if we've got one.
9119 if (!vmobj.hasOwnProperty('datasets')
9120 || vmobj.datasets.length === 0) {
9121
9122 cb();
9123 return;
9124 }
9125 zfs(['set', 'zoned=off', vmobj.datasets[0]], log,
9126 function (err, fds) {
9127
9128 if (err) {
9129 log.error({err: err, stdout: fds.stdout,
9130 stderr: fds.stderr}, 'Unable to turn off "zoned" for '
9131 + vmobj.datasets[0]);
9132 }
9133 cb(err);
9134 });
9135 }, function (cb) {
9136 // if we have a delegated dataset, rename zones/<uuid>/data
9137 // -> zones/<uuid>-reprovisioning-data
9138 if (!vmobj.hasOwnProperty('datasets')
9139 || vmobj.datasets.length === 0) {
9140
9141 cb();
9142 return;
9143 }
9144 zfs(['rename', '-f', vmobj.datasets[0], vmobj.zfs_filesystem
9145 + '-reprovisioning-data'], log, function (err, fds) {
9146
9147 if (err) {
9148 log.error({err: err, stdout: fds.stdout,
9149 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9150 + vmobj.datasets[0]);
9151 }
9152 cb(err);
9153 });
9154 }, function (cb) {
9155 // unmount <zonepath>/cores so dataset is not busy
9156 zfs(['umount', vmobj.zonepath + '/cores'], log,
9157 function (err, fds) {
9158
9159 if (err) {
9160 if (trim(fds.stderr).match(/not a mountpoint$/)) {
9161 log.info('ignoring failure to umount cores which '
9162 + 'wasn\'t mounted');
9163 cb();
9164 return;
9165 } else {
9166 log.error({err: err, stdout: fds.stdout,
9167 stderr: fds.stderr}, 'Unable to umount '
9168 + vmobj.zonepath + '/cores');
9169 }
9170 }
9171 cb(err);
9172 });
9173 }, function (cb) {
9174 // rename <zfs_filesystem> dataset out of the way
9175 zfs(['rename', '-f', vmobj.zfs_filesystem, vmobj.zfs_filesystem
9176 + '-reprovisioning-root'], log, function (err, fds) {
9177
9178 if (err) {
9179 log.error({err: err, stdout: fds.stdout,
9180 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9181 + vmobj.zfs_filesystem);
9182 }
9183 cb(err);
9184 });
9185 }, function (cb) {
9186 var snapname = vmobj.zpool + '/' + payload.image_uuid + '@final';
9187
9188 // ensure we've got our snapshot
9189 zfs(['get', '-Ho', 'value', 'type', snapname], log,
9190 function (err, fds) {
9191
9192 if (!err) {
9193 // snapshot already exists, use it
9194 log.debug('snapshot "' + snapname + '" exists');
9195 snapshot = snapname;
9196 cb();
9197 return;
9198 }
9199
9200 if (fds.stderr.match(/dataset does not exist/)) {
9201 // we'll use a different one. (falls throught to next func)
9202 cb();
9203 } else {
9204 cb(err);
9205 }
9206 });
9207 }, function (cb) {
9208 var snapname;
9209
9210 if (snapshot) {
9211 // already know which one to use, don't create one
9212 cb();
9213 return;
9214 }
9215
9216 snapname = vmobj.zpool + '/' + payload.image_uuid
9217 + '@' + vmobj.uuid;
9218
9219 // ensure we've got a snapshot
9220 zfs(['get', '-Ho', 'value', 'type', snapname], log,
9221 function (err, fds) {
9222
9223 if (!err) {
9224 // snapshot already exists, use it
9225 log.debug('snapshot "' + snapname + '" exists');
9226 snapshot = snapname;
9227 cb();
9228 return;
9229 }
9230
9231 if (fds.stderr.match(/dataset does not exist/)) {
9232 zfs(['snapshot', snapname], log, function (e, snap_fds) {
9233 if (e) {
9234 e.stdout = snap_fds.stdout;
9235 e.stderr = snap_fds.stderr;
9236 log.error(e, 'Failed to create snapshot: '
9237 + e.message);
9238 } else {
9239 log.debug('created snapshot "' + snapname + '"');
9240 snapshot = snapname;
9241 }
9242 cb(e);
9243 });
9244 } else {
9245 cb(err);
9246 return;
9247 }
9248 });
9249 }, function (cb) {
9250 var args;
9251
9252 // clone the new image creating a new dataset for zoneroot
9253 assert(snapshot);
9254
9255 args = ['clone'];
9256 if (vmobj.hasOwnProperty('quota') && vmobj.quota > 0) {
9257 args.push('-o');
9258 args.push('quota=' + vmobj.quota + 'G');
9259 }
9260 args.push(snapshot);
9261 args.push(vmobj.zfs_filesystem);
9262
9263 zfs(args, log, function (err, fds) {
9264 if (err) {
9265 log.error({err: err, stdout: fds.stdout,
9266 stderr: fds.stderr}, 'Unable to create new clone of '
9267 + payload.image_uuid);
9268 }
9269 cb(err);
9270 });
9271 }, function (cb) {
9272 var cmd;
9273
9274 // copy zones/<uuid>-reprovisioning-root/config to
9275 // zones/<uuid>/config so we keep metadata and ipf rules.
9276 try {
9277 fs.mkdirSync(vmobj.zonepath + '/config');
9278 } catch (e) {
9279 if (e.code !== 'EEXIST') {
9280 e.message = 'Unable to recreate ' + vmobj.zonepath
9281 + '/config: ' + e.message;
9282 cb(e);
9283 return;
9284 }
9285 }
9286
9287 cmd = 'cp -pPR '
9288 + vmobj.zonepath + '-reprovisioning-root/config/* '
9289 + vmobj.zonepath + '/config/';
9290
9291 log.debug(cmd);
9292 exec(cmd, function (error, stdout, stderr) {
9293 log.debug({'stdout': stdout, 'stderr': stderr}, 'cp results');
9294 if (error) {
9295 error.stdout = stdout;
9296 error.stderr = stderr;
9297 cb(error);
9298 return;
9299 } else {
9300 cb();
9301 }
9302 });
9303 }, function (cb) {
9304 // destroy <zonepath>-reprovisioning-root, since it's no longer used
9305 zfs(['destroy', '-r', vmobj.zfs_filesystem
9306 + '-reprovisioning-root'], log, function (err, fds) {
9307
9308 if (err) {
9309 log.error({err: err, stdout: fds.stdout,
9310 stderr: fds.stderr}, 'Unable to destroy '
9311 + vmobj.zfs_filesystem + '-reprovisioning-root: '
9312 + err.message);
9313 }
9314 cb(err);
9315 });
9316 }, function (cb) {
9317 // remount /zones/<uuid>/cores
9318 zfs(['mount', vmobj.zpool + '/cores/' + uuid], log,
9319 function (err, fds) {
9320
9321 if (err) {
9322 log.error({err: err, stdout: fds.stdout,
9323 stderr: fds.stderr}, 'Unable to mount ' + vmobj.zonepath
9324 + '/cores: ' + err.message);
9325 }
9326 cb(err);
9327 });
9328 }, function (cb) {
9329 var args = ['-r', '-R', vmobj.zonepath, '-z', vmobj.zonename];
9330 var cmd = BRAND_OPTIONS[vmobj.brand].features.brand_install_script;
9331
9332 // We run the brand's install script here with the -r flag which
9333 // tells it to do everything that's relevant to reprovision.
9334
9335 log.debug(cmd + ' ' + args.join(' '));
9336 execFile(cmd, args, function (error, stdout, stderr) {
9337 var new_err;
9338
9339 if (error) {
9340 new_err = new Error('Error running brand install script '
9341 + cmd);
9342 // error's message includes stderr.
9343 log.error({err: error, stdout: stdout},
9344 'brand install script exited with code ' + error.code);
9345 cb(new_err);
9346 } else {
9347 log.debug(cmd + ' stderr:\n' + stderr);
9348 cb();
9349 }
9350 });
9351 }, function (cb) {
9352 // rename zones/<uuid>-reprovision-data -> zones/<uuid>/data
9353 if (!vmobj.hasOwnProperty('datasets')
9354 || vmobj.datasets.length === 0) {
9355
9356 cb();
9357 return;
9358 }
9359 zfs(['rename', '-f', vmobj.zfs_filesystem + '-reprovisioning-data',
9360 vmobj.datasets[0]], log, function (err, fds) {
9361
9362 if (err) {
9363 log.error({err: err, stdout: fds.stdout,
9364 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9365 + vmobj.zfs_filesystem);
9366 }
9367 cb(err);
9368 });
9369 }, function (cb) {
9370 // set zoned=on for zones/<uuid>/data
9371 if (!vmobj.hasOwnProperty('datasets')
9372 || vmobj.datasets.length === 0) {
9373
9374 cb();
9375 return;
9376 }
9377 zfs(['set', 'zoned=on', vmobj.datasets[0]], log,
9378 function (err, fds) {
9379
9380 if (err) {
9381 log.error({err: err, stdout: fds.stdout,
9382 stderr: fds.stderr}, 'Unable to set "zoned" for: '
9383 + vmobj.datasets[0]);
9384 }
9385 cb(err);
9386 });
9387 }, function (cb) {
9388 // update zone's image_uuid field
9389 var zcfg = 'select attr name=dataset-uuid; set value="'
9390 + payload.image_uuid + '"; end';
9391 zonecfg(['-u', uuid, zcfg], log, function (err, fds) {
9392 if (err) {
9393 log.error({err: err, stdout: fds.stdout,
9394 stderr: fds.stderr}, 'unable to set image_uuid on VM '
9395 + uuid);
9396 }
9397 cb(err);
9398 });
9399 }, function (cb) {
9400 var p = {
9401 autoboot: true,
9402 reprovisioning: true,
9403 uuid: uuid,
9404 zonename: vmobj.zonename,
9405 zonepath: vmobj.zonepath
9406 };
9407
9408 // NOTE: someday we could allow mdata_exec_timeout in the original
9409 // payload to reprovision and then pass it along here.
9410
9411 // other fields used by installZone()
9412 [
9413 'dns_domain',
9414 'hostname',
9415 'quota',
9416 'resolvers',
9417 'tmpfs',
9418 'zfs_filesystem',
9419 'zfs_root_compression',
9420 'zfs_root_recsize'
9421 ].forEach(function (k) {
9422 if (vmobj.hasOwnProperty(k)) {
9423 p[k] = vmobj[k];
9424 }
9425 });
9426
9427 // nics needs to be called add_nics here
9428 if (vmobj.hasOwnProperty('nics')) {
9429 p.add_nics = vmobj.nics;
9430 }
9431
9432 installZone(p, log, function (err) {
9433 log.debug(err, 'ran installZone() for reprovision');
9434 cb(err);
9435 });
9436 }
9437 ], function (err) {
9438 if (err && set_transition) {
9439 // remove transition now, if we failed.
9440 VM.unsetTransition(vmobj, {log: log}, function () {
9441 // err here is original err, we ignore failure to unset because
9442 // nothing we can do about that..
9443 callback(err);
9444 });
9445 } else {
9446 callback(err);
9447 }
9448 });
9449 };
9450
9451 exports.install = function (uuid, options, callback)
9452 {
9453 var log;
9454
9455 // options is optional
9456 if (arguments.length === 2) {
9457 callback = arguments[1];
9458 options = {};
9459 }
9460
9461 ensureLogging(true);
9462 if (options.hasOwnProperty('log')) {
9463 log = options.log;
9464 } else {
9465 log = VM.log.child({action: 'install', vm: uuid});
9466 }
9467
9468 log.info('Installing VM ' + uuid);
9469
9470 fs.readFile('/etc/zones/' + uuid + '-receiving.json',
9471 function (err, data) {
9472 var payload;
9473
9474 if (err) {
9475 callback(err);
9476 return;
9477 }
9478
9479 try {
9480 payload = JSON.parse(data.toString());
9481 } catch (e) {
9482 callback(e);
9483 return;
9484 }
9485
9486 // installZone takes a payload
9487 installZone(payload, log, callback);
9488 }
9489 );
9490
9491 };
9492
9493 function getAllDatasets(vmobj)
9494 {
9495 var datasets = [];
9496 var disk;
9497
9498 if (vmobj.hasOwnProperty('zfs_filesystem')) {
9499 datasets.push(vmobj.zfs_filesystem);
9500 }
9501
9502 for (disk in vmobj.disks) {
9503 disk = vmobj.disks[disk];
9504 if (disk.hasOwnProperty('zfs_filesystem')) {
9505 datasets.push(disk.zfs_filesystem);
9506 }
9507 }
9508
9509 return datasets;
9510 }
9511
9512 //
9513 // Headers are 512 bytes and look like:
9514 //
9515 // MAGIC-VMBUNDLE\0
9516 // <VERSION>\0 -- ASCII #s
9517 // <CHECKSUM>\0 -- ASCII (not yet used)
9518 // <OBJ-NAME>\0 -- max length: 256
9519 // <OBJ-SIZE>\0 -- ASCII # of bytes
9520 // <PADDED-SIZE>\0 -- ASCII # of bytes, must be multiple of 512
9521 // ...\0
9522 //
9523 function chunkHeader(name, size, padding)
9524 {
9525 var header = new Buffer(512);
9526 var pos = 0;
9527
9528 header.fill(0);
9529 pos += addString(header, 'MAGIC-VMBUNDLE', pos);
9530 pos += addString(header, sprintf('%d', 1), pos);
9531 pos += addString(header, 'CHECKSUM', pos);
9532 pos += addString(header, name, pos);
9533 pos += addString(header, sprintf('%d', size), pos);
9534 pos += addString(header, sprintf('%d', size + padding), pos);
9535
9536 return (header);
9537 }
9538
9539 // add the string to buffer at pos, returning pos of new end of the buffer.
9540 function addString(buf, str, pos)
9541 {
9542 var len = str.length;
9543 buf.write(str, pos);
9544 return (len + 1);
9545 }
9546
9547 function sendJSON(target, json, log, cb)
9548 {
9549 var header;
9550 var pad;
9551 var padding = 0;
9552
9553 assert(log, 'no logger passed for sendJSON()');
9554
9555 if (target === 'stdout') {
9556 if ((json.length % 512) != 0) {
9557 padding = 512 - (json.length % 512);
9558 }
9559 header = chunkHeader('JSON', json.length, padding);
9560 process.stdout.write(header);
9561 process.stdout.write(json, 'ascii');
9562 if (padding > 0) {
9563 pad = new Buffer(padding);
9564 pad.fill(0);
9565 process.stdout.write(pad);
9566 }
9567 cb();
9568 } else {
9569 log.error('Don\'t know how to send JSON to '
9570 + JSON.stringify(target));
9571 cb(new Error('Don\'t know how to send JSON to '
9572 + JSON.stringify(target)));
9573 }
9574 }
9575
9576 function sendDataset(target, dataset, log, callback)
9577 {
9578 var header;
9579
9580 assert(log, 'no logger passed for sendDataset()');
9581
9582 if (target === 'stdout') {
9583
9584 async.series([
9585 function (cb) {
9586 // delete any existing 'sending' snapshot
9587 zfs(['destroy', '-F', dataset + '@sending'], log,
9588 function (err, fds) {
9589 // We don't expect this to succeed, since that means
9590 // something left an @sending around. Warn if succeeds.
9591 if (!err) {
9592 log.warn('Destroyed pre-existing ' + dataset
9593 + '@sending');
9594 }
9595 cb();
9596 }
9597 );
9598 }, function (cb) {
9599 zfs(['snapshot', dataset + '@sending'], log,
9600 function (err, fds) {
9601
9602 cb(err);
9603 });
9604 }, function (cb) {
9605 header = chunkHeader(dataset, 0, 0);
9606 process.stdout.write(header);
9607 cb();
9608 }, function (cb) {
9609 var child;
9610
9611 child = spawn('/usr/sbin/zfs',
9612 ['send', '-p', dataset + '@sending'],
9613 {customFds: [-1, 1, -1]});
9614 child.stderr.on('data', function (data) {
9615 var idx;
9616 var lines = trim(data.toString()).split('\n');
9617
9618 for (idx in lines) {
9619 log.debug('zfs send: ' + trim(lines[idx]));
9620 }
9621 });
9622 child.on('close', function (code) {
9623 log.debug('zfs send process exited with code '
9624 + code);
9625 cb();
9626 });
9627 }, function (cb) {
9628 zfs(['destroy', '-F', dataset + '@sending'], log,
9629 function (err, fds) {
9630 if (err) {
9631 log.warn(err, 'Unable to destroy ' + dataset
9632 + '@sending: ' + err.message);
9633 }
9634 cb(err);
9635 }
9636 );
9637 }
9638 ], function (err) {
9639 if (err) {
9640 log.error(err, 'Failed to send dataset: ' + err.message);
9641 } else {
9642 log.info('Successfully sent dataset');
9643 }
9644 callback(err);
9645 });
9646 } else {
9647 log.error('Don\'t know how to send datasets to '
9648 + JSON.stringify(target));
9649 callback(new Error('Don\'t know how to send datasets to '
9650 + JSON.stringify(target)));
9651 }
9652 }
9653
9654 exports.send = function (uuid, target, options, callback)
9655 {
9656 var datasets;
9657 var log;
9658 var vmobj;
9659
9660 // options is optional
9661 if (arguments.length === 3) {
9662 callback = arguments[2];
9663 options = {};
9664 }
9665
9666 ensureLogging(true);
9667 if (options.hasOwnProperty('log')) {
9668 log = options.log;
9669 } else {
9670 log = VM.log.child({action: 'send', vm: uuid});
9671 }
9672
9673 target = 'stdout';
9674
9675 log.info('Sending VM ' + uuid + ' to: ' + JSON.stringify(target));
9676 async.series([
9677 function (cb) {
9678 // make sure we *can* send first, to avoid wasting cycles
9679 if (target === 'stdout' && tty.isatty(1)) {
9680 log.error('Cannot send VM to a TTY.');
9681 cb(new Error('Cannot send VM to a TTY.'));
9682 } else {
9683 cb();
9684 }
9685 }, function (cb) {
9686 // NOTE: for this load we always load all fields, because we need
9687 // to send them all to the target machine.
9688 VM.load(uuid, {log: log}, function (err, obj) {
9689 if (err) {
9690 cb(err);
9691 } else {
9692 vmobj = obj;
9693 cb();
9694 }
9695 });
9696 }, function (cb) {
9697 datasets = getAllDatasets(vmobj);
9698 if (datasets.length < 1) {
9699 log.error('Cannot send VM with no datasets.');
9700 cb(new Error('VM has no datasets.'));
9701 } else {
9702 cb();
9703 }
9704 }, function (cb) {
9705 if (vmobj.state !== 'stopped') {
9706 // In this case we need to stop it and make sure it stopped.
9707 VM.stop(uuid, {log: log}, function (e) {
9708 if (e) {
9709 log.error(e, 'unable to stop VM ' + uuid + ': '
9710 + e.message);
9711 cb(e);
9712 return;
9713 }
9714 VM.load(uuid, {fields: ['zone_state', 'uuid'], log: log},
9715 function (error, obj) {
9716
9717 if (error) {
9718 log.error(error, 'unable to reload VM ' + uuid
9719 + ': ' + error.message);
9720 return;
9721 }
9722 if (obj.zone_state !== 'installed') {
9723 log.error('after stop attempt, state is '
9724 + obj.zone_state + ' != installed');
9725 cb(new Error('state after stopping is '
9726 + obj.zone_state + ' != installed'));
9727 return;
9728 }
9729 cb();
9730 });
9731 });
9732 } else {
9733 // already stopped, good to go!
9734 cb();
9735 }
9736 }, function (cb) {
9737 // Clean up trash left from broken datasets (see OS-388)
9738 try {
9739 fs.unlinkSync(vmobj.zonepath + '/SUNWattached.xml');
9740 } catch (err) {
9741 // DO NOTHING, this file shouldn't have existed anyway.
9742 }
9743 try {
9744 fs.unlinkSync(vmobj.zonepath + '/SUNWdetached.xml');
9745 } catch (err) {
9746 // DO NOTHING, this file shouldn't have existed anyway.
9747 }
9748 cb();
9749 }, function (cb) {
9750 // send JSON
9751 var json = JSON.stringify(vmobj, null, 2) + '\n';
9752 sendJSON(target, json, log, cb);
9753 }, function (cb) {
9754 // send datasets
9755 async.forEachSeries(datasets, function (ds, c) {
9756 sendDataset(target, ds, log, c);
9757 }, function (e) {
9758 if (e) {
9759 log.error('Failed to send datasets');
9760 }
9761 cb(e);
9762 });
9763 }
9764 ], function (err) {
9765 callback(err);
9766 });
9767 };
9768
9769 exports.create = function (payload, options, callback)
9770 {
9771 var log;
9772
9773 // options is optional
9774 if (arguments.length === 2) {
9775 callback = arguments[1];
9776 options = {};
9777 }
9778
9779 ensureLogging(true);
9780
9781 if (options.hasOwnProperty('log')) {
9782 log = options.log;
9783 } else {
9784 // default to VM.log until we have a uuid, then we'll switch.
9785 log = VM.log;
9786 }
9787
9788 log.info('Creating VM, original payload:\n'
9789 + JSON.stringify(payload, null, 2));
9790
9791 async.waterfall([
9792 function (cb) {
9793 // We get a UUID first so that we can attach as many log messages
9794 // as possible to this uuid. Since we don't have a UUID here, we
9795 // send VM.log as the logger. We'll switch to a log.child as soon
9796 // as we have uuid.
9797 createZoneUUID(payload, log, function (e, uuid) {
9798 // either payload will have .uuid or we'll return error here.
9799 cb(e);
9800 });
9801 }, function (cb) {
9802 // If we got here, payload now has .uuid and we can start logging
9803 // messages with that uuid if we didn't already have a logger.
9804 if (!options.hasOwnProperty('log')) {
9805 log = VM.log.child({action: 'create', vm: payload.uuid});
9806 }
9807 cb();
9808 }, function (cb) {
9809 normalizePayload(payload, null, log, function (err) {
9810 if (err) {
9811 log.error(err, 'Failed to validate payload: '
9812 + err.message);
9813 } else {
9814 log.debug('normalized payload:\n'
9815 + JSON.stringify(payload, null, 2));
9816 }
9817 cb(err);
9818 });
9819 }, function (cb) {
9820 checkDatasetProvisionable(payload, log, function (provisionable) {
9821 if (!provisionable) {
9822 log.error('checkDatasetProvisionable() says dataset is '
9823 + 'unprovisionable');
9824 cb(new Error('provisioning dataset ' + payload.image_uuid
9825 + ' with brand ' + payload.brand
9826 + ' is not supported'));
9827 return;
9828 }
9829 cb();
9830 });
9831 }, function (cb) {
9832 if (BRAND_OPTIONS[payload.brand].features.type === 'KVM') {
9833 createVM(payload, log, function (error, result) {
9834 if (error) {
9835 cb(error);
9836 } else {
9837 cb(null, {'uuid': payload.uuid,
9838 'zonename': payload.zonename});
9839 }
9840 });
9841 } else {
9842 createZone(payload, log, function (error, result) {
9843 if (error) {
9844 cb(error);
9845 } else {
9846 cb(null, {'uuid': payload.uuid,
9847 'zonename': payload.zonename});
9848 }
9849 });
9850 }
9851 }
9852 ], function (err, obj) {
9853 callback(err, obj);
9854 });
9855 };
9856
9857 // delete a zvol
9858 function deleteVolume(volume, log, callback)
9859 {
9860 var args;
9861 var origin;
9862
9863 assert(log, 'no logger passed to deleteVolume()');
9864
9865 if (volume.missing) {
9866 // this volume doesn't actually exist, so skip trying to delete.
9867 log.info('volume ' + volume.path + ' doesn\'t exist, skipping '
9868 + 'deletion');
9869 callback();
9870 return;
9871 }
9872
9873 async.series([
9874 function (cb) {
9875 args = ['get', '-Ho', 'value', 'origin', volume.zfs_filesystem];
9876 zfs(args, log, function (err, fds) {
9877 if (err && fds.stderr.match('dataset does not exist')) {
9878 log.info('volume ' + volume.path + ' doesn\'t exist, '
9879 + 'skipping deletion');
9880 cb();
9881 } else {
9882 origin = trim(fds.stdout);
9883 log.info('found origin "' + origin + '"');
9884 cb(err);
9885 }
9886 });
9887 }, function (cb) {
9888 // use recursive delete to handle possible snapshots on volume
9889 args = ['destroy', '-rF', volume.zfs_filesystem];
9890 zfs(args, log, function (err, fds) {
9891 // err will be non-null if something broke
9892 cb(err);
9893 });
9894 }, function (cb) {
9895 // we never delete an @final snapshot, that's the one from recv
9896 // that imgadm left around for us on purpose.
9897 if (!origin || origin.length < 1 || origin == '-'
9898 || origin.match('@final')) {
9899
9900 cb();
9901 return;
9902 }
9903 args = ['destroy', '-rF', origin];
9904 zfs(args, log, function (err, fds) {
9905 // err will be non-null if something broke
9906 cb(err);
9907 });
9908 }
9909 ], function (err) {
9910 callback(err);
9911 });
9912 }
9913
9914 function deleteZone(uuid, log, callback)
9915 {
9916 var load_fields;
9917 var vmobj;
9918
9919 assert(log, 'no logger passed to deleteZone()');
9920
9921 load_fields = [
9922 'archive_on_delete',
9923 'disks',
9924 'uuid',
9925 'zonename'
9926 ];
9927
9928 async.series([
9929 function (cb) {
9930 VM.load(uuid, {fields: load_fields, log: log}, function (err, obj) {
9931 if (err) {
9932 cb(err);
9933 return;
9934 }
9935 vmobj = obj;
9936 cb();
9937 });
9938 }, function (cb) {
9939 log.debug('archive_on_delete is set to '
9940 + !!vmobj.archive_on_delete);
9941 if (!vmobj.archive_on_delete) {
9942 cb();
9943 return;
9944 }
9945 archiveVM(vmobj.uuid, log, function () {
9946 cb();
9947 });
9948 // TODO: replace these next two with VM.stop(..{force: true} ?
9949 }, function (cb) {
9950 log.debug('setting autoboot=false');
9951 zonecfg(['-u', uuid, 'set autoboot=false'], log, function (e, fds) {
9952 if (e) {
9953 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9954 'Error setting autoboot=false');
9955 } else {
9956 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9957 'set autoboot=false');
9958 }
9959 cb();
9960 });
9961 }, function (cb) {
9962 log.debug('halting zone');
9963 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) {
9964 if (e) {
9965 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9966 'Error halting zone');
9967 } else {
9968 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9969 'halted zone');
9970 }
9971 cb();
9972 });
9973 }, function (cb) {
9974 log.debug('uninstalling zone');
9975 zoneadm(['-u', uuid, 'uninstall', '-F'], log, function (e, fds) {
9976 if (e) {
9977 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9978 'Error uninstalling zone: ' + e.message);
9979 } else {
9980 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9981 'uninstalled zone');
9982 }
9983 cb();
9984 });
9985 }, function (cb) {
9986 function loggedDeleteVolume(volume, callbk) {
9987 return deleteVolume(volume, log, callbk);
9988 }
9989
9990 if (vmobj && vmobj.hasOwnProperty('disks')) {
9991 async.forEachSeries(vmobj.disks, loggedDeleteVolume,
9992 function (err) {
9993 if (err) {
9994 log.error(err, 'Unknown error deleting volumes: '
9995 + err.message);
9996 cb(err);
9997 } else {
9998 log.info('successfully deleted volumes');
9999 cb();
10000 }
10001 }
10002 );
10003 } else {
10004 log.debug('skipping volume destruction for diskless '
10005 + vmobj.uuid);
10006 cb();
10007 }
10008 }, function (cb) {
10009 if (vmobj.zonename) {
10010 log.debug('deleting zone');
10011 // XXX for some reason -u <uuid> doesn't work with delete
10012 zonecfg(['-z', vmobj.zonename, 'delete', '-F'], log,
10013 function (e, fds) {
10014
10015 if (e) {
10016 log.warn({err: e, stdout: fds.stdout,
10017 stderr: fds.stderr}, 'Error deleting VM');
10018 } else {
10019 log.debug({stdout: fds.stdout, stderr: fds.stderr},
10020 'deleted VM ' + uuid);
10021 }
10022 cb();
10023 });
10024 } else {
10025 cb();
10026 }
10027 }, function (cb) {
10028 VM.load(uuid, {fields: ['uuid'], log: log, missing_ok: true},
10029 function (err, obj) {
10030
10031 var gone = /^zoneadm:.*: No such zone configured/;
10032 if (err && err.message.match(gone)) {
10033 // the zone is gone, that's good.
10034 log.debug('confirmed VM is gone.');
10035 cb();
10036 } else if (err) {
10037 // there was a non-expected error.
10038 cb(err);
10039 } else {
10040 // the VM still exists!
10041 err = new Error('VM still exists after delete.');
10042 err.code = 'EEXIST';
10043 cb(err);
10044 }
10045 });
10046 }, function (cb) {
10047 // delete the incoming payload if it exists
10048 fs.unlink('/etc/zones/' + vmobj.uuid + '-receiving.json',
10049 function (e) {
10050 // we can't do anyhing if this fails other than log
10051 if (e && e.code !== 'ENOENT') {
10052 log.warn(e, 'Failed to delete ' + vmobj.uuid
10053 + '-receiving.json (' + e.code + '): ' + e.message);
10054 }
10055 cb();
10056 }
10057 );
10058 }
10059 ], function (error) {
10060 callback(error);
10061 });
10062 }
10063
10064 exports.delete = function (uuid, options, callback)
10065 {
10066 var attemptDelete;
10067 var last_try = 16;
10068 var log;
10069 var next_try = 1;
10070 var tries = 0;
10071
10072 // options is optional
10073 if (arguments.length === 2) {
10074 callback = arguments[1];
10075 options = {};
10076 }
10077
10078 ensureLogging(true);
10079
10080 if (options.hasOwnProperty('log')) {
10081 log = options.log;
10082 } else {
10083 log = VM.log.child({action: 'delete', vm: uuid});
10084 }
10085
10086 log.info('Deleting VM ' + uuid);
10087
10088 attemptDelete = function (cb) {
10089 next_try = (next_try * 2);
10090 deleteZone(uuid, log, function (err) {
10091 tries++;
10092 if (err && err.code === 'EEXIST') {
10093 // zone still existed, try again if we've not tried too much.
10094 if (next_try <= last_try) {
10095 log.info('VM.delete(' + tries + '): still there, '
10096 + 'will try again in: ' + next_try + ' secs');
10097 setTimeout(function () {
10098 // try again
10099 attemptDelete(cb);
10100 }, next_try * 1000);
10101 } else {
10102 log.warn('VM.delete(' + tries + '): still there after'
10103 + ' ' + next_try + ' seconds, giving up.');
10104 cb(new Error('delete failed after ' + tries + ' attempts. '
10105 + '(check the log for details)'));
10106 return;
10107 }
10108 } else if (err) {
10109 // error but not one we can retry from.
10110 log.error(err, 'VM.delete: FATAL: ' + err.message);
10111 cb(err);
10112 } else {
10113 // success!
10114 log.debug('VM.delete: SUCCESS');
10115 cb();
10116 }
10117 });
10118 };
10119
10120 attemptDelete(function (err) {
10121 if (err) {
10122 log.error(err);
10123 }
10124 callback(err);
10125 });
10126 };
10127
10128 // This function needs vmobj to have:
10129 //
10130 // brand
10131 // never_booted
10132 // uuid
10133 // zonename
10134 //
10135 function startZone(vmobj, log, callback)
10136 {
10137 var set_autoboot = 'set autoboot=true';
10138 var uuid = vmobj.uuid;
10139
10140 assert(log, 'no logger passed to startZone()');
10141
10142 log.debug('startZone starting ' + uuid);
10143
10144 //
10145 // We set autoboot (or vm-autoboot) here because we've just intentionally
10146 // started this vm, so we want it to come up if the host is rebooted.
10147 //
10148 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
10149 set_autoboot = 'select attr name=vm-autoboot; set value=true; end';
10150 }
10151
10152 async.series([
10153 function (cb) {
10154 // do the booting
10155 zoneadm(['-u', uuid, 'boot', '-X'], log, function (err, boot_fds) {
10156 if (err) {
10157 log.error({err: err, stdout: boot_fds.stdout,
10158 stderr: boot_fds.stderr}, 'zoneadm failed to boot '
10159 + 'VM');
10160 } else {
10161 log.debug({stdout: boot_fds.stdout,
10162 stderr: boot_fds.stderr}, 'zoneadm booted VM');
10163 }
10164 cb(err);
10165 });
10166 }, function (cb) {
10167 // ensure it booted
10168 VM.waitForZoneState(vmobj, 'running', {timeout: 30, log: log},
10169 function (err, result) {
10170
10171 if (err) {
10172 if (err.code === 'ETIMEOUT') {
10173 log.info(err, 'timeout waiting for zone to go to '
10174 + '"running"');
10175 } else {
10176 log.error(err, 'unknown error waiting for zone to go'
10177 + ' "running"');
10178 }
10179 } else {
10180 // zone got to running
10181 log.info('VM seems to have switched to "running"');
10182 }
10183 cb(err);
10184 });
10185 }, function (cb) {
10186 zonecfg(['-u', uuid, set_autoboot], log,
10187 function (err, autoboot_fds) {
10188
10189 if (err) {
10190 // The vm is running at this point, erroring out here would
10191 // do no good, so we just log it.
10192 log.error({err: err, stdout: autoboot_fds.stdout,
10193 stderr: autoboot_fds.stderr}, 'startZone(): Failed to '
10194 + set_autoboot + ' for ' + uuid);
10195 } else {
10196 log.debug({stdout: autoboot_fds.stdout,
10197 stderr: autoboot_fds.stderr}, 'set autoboot');
10198 }
10199 cb(err);
10200 });
10201 }, function (cb) {
10202 if (!vmobj.never_booted) {
10203 cb();
10204 return;
10205 }
10206 zonecfg(['-u', uuid, 'remove attr name=never-booted' ], log,
10207 function (err, neverbooted_fds) {
10208 // Ignore errors here, because we're started.
10209 if (err) {
10210 log.warn({err: err, stdout: neverbooted_fds.stdout,
10211 stderr: neverbooted_fds.stderr}, 'failed to remove '
10212 + 'never-booted flag');
10213 } else {
10214 log.debug({stdout: neverbooted_fds.stdout,
10215 stderr: neverbooted_fds.stderr}, 'removed '
10216 + 'never-booted flag');
10217 }
10218 cb();
10219 }
10220 );
10221 }
10222 ], function (err) {
10223 if (!err) {
10224 log.info('Started ' + uuid);
10225 }
10226 callback(err);
10227 });
10228 }
10229
10230 // build the qemu cmdline and start up a VM
10231 //
10232 // vmobj needs any of the following that are defined:
10233 //
10234 // boot
10235 // brand
10236 // cpu_type
10237 // default_gateway
10238 // disks
10239 // hostname
10240 // internal_metadata
10241 // never_booted
10242 // nics
10243 // qemu_extra_opts
10244 // qemu_opts
10245 // ram
10246 // resolvers
10247 // spice_opts
10248 // spice_password
10249 // spice_port
10250 // state
10251 // uuid
10252 // vcpus
10253 // vga
10254 // virtio_txtimer
10255 // virtio_txburst
10256 // vnc_password
10257 // zone_state
10258 // zonename
10259 // zonepath
10260 //
10261 function startVM(vmobj, extra, log, callback)
10262 {
10263 var check_path;
10264 var cmdargs = [];
10265 var d;
10266 var defaultgw = '';
10267 var disk;
10268 var diskargs = '';
10269 var disk_idx = 0;
10270 var found;
10271 var hostname = vmobj.uuid;
10272 var mdata;
10273 var nic;
10274 var nic_idx = 0;
10275 var primary_found = false;
10276 var qemu_opts = '';
10277 var r;
10278 var script;
10279 var spiceargs;
10280 var uuid = vmobj.uuid;
10281 var virtio_txburst;
10282 var virtio_txtimer;
10283 var vnic_opts;
10284 var zoneroot;
10285
10286 assert(log, 'no logger passed to startVM');
10287 assert(vmobj.hasOwnProperty('zonepath'), 'missing zonepath');
10288
10289 log.debug('startVM(' + uuid + ')');
10290
10291 if (!vmobj.hasOwnProperty('state')) {
10292 callback(new Error('Cannot start VM ' + uuid + ' which has no state'));
10293 return;
10294 }
10295
10296 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning')
10297 || (vmobj.state === 'provisioning'
10298 && vmobj.zone_state !== 'installed')) {
10299
10300 callback(new Error('Cannot start VM from state: ' + vmobj.state
10301 + ', must be "stopped"'));
10302 return;
10303 }
10304
10305 zoneroot = path.join(vmobj.zonepath, '/root');
10306
10307 // We're going to write to /startvm and /tmp/vm.metadata, we don't care if
10308 // they already exist, but we don't want them to be symlinks.
10309 try {
10310 assertSafeZonePath(zoneroot, '/startvm',
10311 {type: 'file', enoent_ok: true});
10312 assertSafeZonePath(zoneroot, '/tmp/vm.metadata',
10313 {type: 'file', enoent_ok: true});
10314 } catch (e) {
10315 log.error(e, 'Error validating files for startVM(): '
10316 + e.message);
10317 callback(e);
10318 return;
10319 }
10320
10321 // XXX TODO: validate vmobj data is ok to start
10322
10323 cmdargs.push('-m', vmobj.ram);
10324 cmdargs.push('-name', vmobj.uuid);
10325 cmdargs.push('-uuid', vmobj.uuid);
10326
10327 if (vmobj.hasOwnProperty('cpu_type')) {
10328 cmdargs.push('-cpu', vmobj.cpu_type);
10329 } else {
10330 cmdargs.push('-cpu', 'qemu64');
10331 }
10332
10333 if (vmobj.vcpus > 1) {
10334 cmdargs.push('-smp', vmobj.vcpus);
10335 }
10336
10337 for (disk in vmobj.disks) {
10338 if (vmobj.disks.hasOwnProperty(disk)) {
10339 disk = vmobj.disks[disk];
10340 if (!disk.media) {
10341 disk.media = 'disk';
10342 }
10343 diskargs = 'file=' + disk.path + ',if=' + disk.model
10344 + ',index=' + disk_idx + ',media=' + disk.media;
10345 if (disk.boot) {
10346 diskargs = diskargs + ',boot=on';
10347 }
10348 cmdargs.push('-drive', diskargs);
10349 disk_idx++;
10350 }
10351 }
10352
10353 // extra payload can include additional disks that we want to include only
10354 // on this one boot. It can also contain a boot parameter to control boot
10355 // device. See qemu http://qemu.weilnetz.de/qemu-doc.html for info on
10356 // -boot options.
10357 if (extra.hasOwnProperty('disks')) {
10358 for (disk in extra.disks) {
10359 if (extra.disks.hasOwnProperty(disk)) {
10360 disk = extra.disks[disk];
10361
10362 // ensure this is either a disk that gets mounted in or a
10363 // file that's been dropped in to the zonepath
10364 found = false;
10365 for (d in vmobj.disks) {
10366 if (!found && vmobj.disks.hasOwnProperty(d)) {
10367 d = vmobj.disks[d];
10368 if (d.path === disk.path) {
10369 found = true;
10370 }
10371 }
10372 }
10373 check_path = path.join(vmobj.zonepath, 'root', disk.path);
10374 if (!found && fs.existsSync(check_path)) {
10375 found = true;
10376 }
10377 if (!found) {
10378 callback(new Error('Cannot find disk: ' + disk.path));
10379 return;
10380 }
10381
10382 if (!disk.media) {
10383 disk.media = 'disk';
10384 }
10385 diskargs = 'file=' + disk.path + ',if=' + disk.model
10386 + ',index=' + disk_idx + ',media=' + disk.media;
10387 if (disk.boot) {
10388 diskargs = diskargs + ',boot=on';
10389 }
10390 cmdargs.push('-drive', diskargs);
10391 disk_idx++;
10392 }
10393 }
10394 }
10395
10396 // helpful values:
10397 // order=nc (network boot, then fallback to disk)
10398 // once=d (boot on disk once and the fallback to default)
10399 // order=c,once=d (boot on CDROM this time, but not subsequent boots)
10400 if (extra.hasOwnProperty('boot')) {
10401 cmdargs.push('-boot', extra.boot);
10402 } else if (vmobj.hasOwnProperty('boot')) {
10403 cmdargs.push('-boot', vmobj.boot);
10404 } else {
10405 // order=cd means try harddisk first (c) and cdrom if that fails (d)
10406 cmdargs.push('-boot', 'order=cd');
10407 }
10408
10409 if (vmobj.hasOwnProperty('hostname')) {
10410 hostname = vmobj.hostname;
10411 }
10412
10413 if (vmobj.hasOwnProperty('default_gateway')) {
10414 defaultgw = vmobj['default_gateway'];
10415 }
10416
10417 /*
10418 * These tunables are set for all virtio vnics on this VM.
10419 */
10420 virtio_txtimer = VIRTIO_TXTIMER_DEFAULT;
10421 virtio_txburst = VIRTIO_TXBURST_DEFAULT;
10422 if (vmobj.hasOwnProperty('virtio_txtimer')) {
10423 virtio_txtimer = vmobj.virtio_txtimer;
10424 }
10425 if (vmobj.hasOwnProperty('virtio_txburst')) {
10426 virtio_txburst = vmobj.virtio_txburst;
10427 }
10428
10429 for (nic in vmobj.nics) {
10430 if (vmobj.nics.hasOwnProperty(nic)) {
10431 nic = vmobj.nics[nic];
10432
10433 // for virtio devices, we want to be able to set the txtimer and
10434 // txburst so we use a '-device' instead of a '-net' line.
10435 if (nic.model === 'virtio') {
10436 cmdargs.push('-device',
10437 'virtio-net-pci,mac=' + nic.mac
10438 + ',tx=timer,x-txtimer=' + virtio_txtimer
10439 + ',x-txburst=' + virtio_txburst
10440 + ',vlan=' + nic_idx);
10441 } else {
10442 cmdargs.push('-net',
10443 'nic,macaddr=' + nic.mac
10444 + ',vlan=' + nic_idx
10445 + ',name=' + nic.interface
10446 + ',model=' + nic.model);
10447 }
10448 vnic_opts = 'vnic,name=' + nic.interface
10449 + ',vlan=' + nic_idx
10450 + ',ifname=' + nic.interface;
10451
10452 if (nic.ip != 'dhcp') {
10453 vnic_opts = vnic_opts
10454 + ',ip=' + nic.ip
10455 + ',netmask=' + nic.netmask;
10456 }
10457
10458 // The primary network provides the resolvers, default gateway
10459 // and hostname to prevent vm from trying to use settings
10460 // from more than one nic
10461 if (!primary_found) {
10462 if (nic.hasOwnProperty('primary') && nic.primary) {
10463 if (nic.hasOwnProperty('gateway') && nic.ip != 'dhcp') {
10464 vnic_opts += ',gateway_ip=' + nic.gateway;
10465 }
10466 primary_found = true;
10467 } else if (defaultgw && nic.hasOwnProperty('gateway')
10468 && nic.gateway == defaultgw) {
10469
10470 /*
10471 * XXX this exists here for backward compatibilty. New VMs
10472 * and old VMs that are upgraded should not use
10473 * default_gateway. When we've implemented autoupgrade
10474 * this block (and all reference to default_gateway)
10475 * can be removed.
10476 */
10477
10478 if (nic.ip != 'dhcp') {
10479 vnic_opts += ',gateway_ip=' + nic.gateway;
10480 }
10481 primary_found = true;
10482 }
10483
10484 if (primary_found && nic.ip != 'dhcp') {
10485 if (hostname) {
10486 vnic_opts += ',hostname=' + hostname;
10487 }
10488 if (vmobj.hasOwnProperty('resolvers')) {
10489 for (r in vmobj.resolvers) {
10490 vnic_opts += ',dns_ip' + r + '='
10491 + vmobj.resolvers[r];
10492 }
10493 }
10494 }
10495 }
10496
10497 cmdargs.push('-net', vnic_opts);
10498 nic_idx++;
10499 }
10500 }
10501
10502 cmdargs.push('-smbios', 'type=1,manufacturer=Joyent,'
10503 + 'product=SmartDC HVM,version=6.2012Q1,'
10504 + 'serial=' + vmobj.uuid + ',uuid=' + vmobj.uuid + ','
10505 + 'sku=001,family=Virtual Machine');
10506
10507 cmdargs.push('-pidfile', '/tmp/vm.pid');
10508
10509 if (vmobj.hasOwnProperty('vga')) {
10510 cmdargs.push('-vga', vmobj.vga);
10511 } else {
10512 cmdargs.push('-vga', 'std');
10513 }
10514
10515 cmdargs.push('-chardev',
10516 'socket,id=qmp,path=/tmp/vm.qmp,server,nowait');
10517 cmdargs.push('-qmp', 'chardev:qmp');
10518
10519 // serial0 is for serial console
10520 cmdargs.push('-chardev',
10521 'socket,id=serial0,path=/tmp/vm.console,server,nowait');
10522 cmdargs.push('-serial', 'chardev:serial0');
10523
10524 // serial1 is used for metadata API
10525 cmdargs.push('-chardev',
10526 'socket,id=serial1,path=/tmp/vm.ttyb,server,nowait');
10527 cmdargs.push('-serial', 'chardev:serial1');
10528
10529 if (!vmobj.qemu_opts) {
10530 if (vmobj.hasOwnProperty('vnc_password')
10531 && vmobj.vnc_password.length > 0) {
10532
10533 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc,password');
10534 } else {
10535 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc');
10536 }
10537 if (vmobj.hasOwnProperty('spice_port')
10538 && vmobj.spice_port !== -1) {
10539
10540 spiceargs = 'sock=/tmp/vm.spice';
10541 if (!vmobj.hasOwnProperty('spice_password')
10542 || vmobj.spice_password.length <= 0) {
10543
10544 spiceargs = spiceargs + ',disable-ticketing';
10545
10546 // Otherwise, spice password is set via qmp, so we don't
10547 // need to do anything here
10548 }
10549 if (vmobj.hasOwnProperty('spice_opts')
10550 && vmobj.spice_opts.length > 0) {
10551
10552 spiceargs = spiceargs + ',' + vmobj.spice_opts;
10553 }
10554 cmdargs.push('-spice', spiceargs);
10555 }
10556 cmdargs.push('-parallel', 'none');
10557 cmdargs.push('-usb');
10558 cmdargs.push('-usbdevice', 'tablet');
10559 cmdargs.push('-k', 'en-us');
10560 } else {
10561 qemu_opts = vmobj.qemu_opts.toString();
10562 }
10563
10564 if (vmobj.qemu_extra_opts) {
10565 qemu_opts = qemu_opts + ' ' + vmobj.qemu_extra_opts;
10566 }
10567
10568 // This actually creates the qemu process
10569 script = '#!/usr/bin/bash\n\n'
10570 + 'exec >/tmp/vm.startvm.log 2>&1\n\n'
10571 + 'set -o xtrace\n\n'
10572 + 'if [[ -x /startvm.zone ]]; then\n'
10573 + ' exec /smartdc/bin/qemu-exec /startvm.zone "'
10574 + cmdargs.join('" "')
10575 + '" ' + qemu_opts + '\n'
10576 + 'else\n'
10577 + ' exec /smartdc/bin/qemu-exec /smartdc/bin/qemu-system-x86_64 "'
10578 + cmdargs.join('" "')
10579 + '" ' + qemu_opts + '\n'
10580 + 'fi\n\n'
10581 + 'exit 1\n';
10582
10583 try {
10584 fs.writeFileSync(vmobj.zonepath + '/root/startvm', script);
10585 fs.chmodSync(vmobj.zonepath + '/root/startvm', '0755');
10586 } catch (e) {
10587 log.warn(e, 'Unable to create /startvm script in ' + vmobj.uuid);
10588 callback(new Error('cannot create /startvm'));
10589 return;
10590 }
10591
10592 mdata = {
10593 'internal_metadata':
10594 vmobj.internal_metadata ? vmobj.internal_metadata : {}
10595 };
10596 fs.writeFile(path.join(vmobj.zonepath, '/root/tmp/vm.metadata'),
10597 JSON.stringify(mdata, null, 2) + '\n',
10598 function (err) {
10599 if (err) {
10600 log.debug(err, 'FAILED TO write metadata to '
10601 + '/tmp/vm.metadata: ' + err.message);
10602 callback(err);
10603 } else {
10604 log.debug('wrote metadata to /tmp/vm.metadata');
10605 startZone(vmobj, log, callback);
10606 }
10607 }
10608 );
10609 }
10610
10611 // according to usr/src/common/zfs/zfs_namecheck.c allowed characters are:
10612 //
10613 // alphanumeric characters plus the following: [-_.:%]
10614 //
10615 function validSnapshotName(snapname, log)
10616 {
10617 assert(log, 'no logger passed to validSnapshotName()');
10618
10619 if (snapname.length < 1 || snapname.length > MAX_SNAPNAME_LENGTH) {
10620 log.error('Invalid snapname length: ' + snapname.length
10621 + ' valid range: [1-' + MAX_SNAPNAME_LENGTH + ']');
10622 return (false);
10623 }
10624
10625 if (snapname.match(/[^a-zA-Z0-9\-\_\.\:\%]/)) {
10626 log.error('Invalid snapshot name: contains invalid characters.');
10627 return (false);
10628 }
10629
10630 return (true);
10631 }
10632
10633 function performSnapshotRollback(snapshots, log, callback)
10634 {
10635 assert(log, 'no logger passed to performSnapshotRollback()');
10636
10637 // NOTE: we assume machine is stopped and snapshots are already validated
10638
10639 function rollback(snapname, cb) {
10640 var args;
10641
10642 args = ['rollback', '-r', snapname];
10643 zfs(args, log, function (zfs_err, fds) {
10644 if (zfs_err) {
10645 log.error({'err': zfs_err, 'stdout': fds.stdout,
10646 'stderr': fds.stdout}, 'zfs rollback of ' + snapname
10647 + ' failed.');
10648 cb(zfs_err);
10649 return;
10650 }
10651 log.info('rolled back snapshot ' + snapname);
10652 log.debug('zfs destroy stdout: ' + fds.stdout);
10653 log.debug('zfs destroy stderr: ' + fds.stderr);
10654 cb();
10655 });
10656 }
10657
10658 async.forEachSeries(snapshots, rollback, function (err) {
10659 if (err) {
10660 log.error(err, 'Unable to rollback some datasets.');
10661 }
10662 callback(err);
10663 });
10664 }
10665
10666 function updateZonecfgTimestamp(vmobj, callback)
10667 {
10668 var file;
10669 var now;
10670
10671 assert(vmobj.zonename, 'updateZonecfgTimestamp() vmobj must have '
10672 + '.zonename');
10673
10674 file = path.join('/etc/zones/', vmobj.zonename + '.xml');
10675 now = new Date();
10676
10677 fs.utimes(file, now, now, callback);
10678 }
10679
10680 exports.rollback_snapshot = function (uuid, snapname, options, callback)
10681 {
10682 var load_fields;
10683 var log;
10684
10685 // options is optional
10686 if (arguments.length === 3) {
10687 callback = arguments[2];
10688 options = {};
10689 }
10690
10691 ensureLogging(true);
10692 if (options.hasOwnProperty('log')) {
10693 log = options.log;
10694 } else {
10695 log = VM.log.child({action: 'rollback_snapshot', vm: uuid});
10696 }
10697
10698 if (!validSnapshotName(snapname, log)) {
10699 callback(new Error('Invalid snapshot name'));
10700 return;
10701 }
10702
10703 load_fields = [
10704 'brand',
10705 'snapshots',
10706 'zfs_filesystem',
10707 'state',
10708 'uuid'
10709 ];
10710
10711 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10712 var found;
10713 var snap;
10714 var snapshot_list = [];
10715
10716 if (err) {
10717 callback(err);
10718 return;
10719 }
10720
10721 if (vmobj.brand === 'kvm') {
10722 callback(new Error('snapshots for KVM VMs currently unsupported'));
10723 return;
10724 }
10725
10726 found = false;
10727 if (vmobj.hasOwnProperty('snapshots')) {
10728 for (snap in vmobj.snapshots) {
10729 if (vmobj.snapshots[snap].name === snapname) {
10730 found = true;
10731 break;
10732 }
10733 }
10734 }
10735 if (!found) {
10736 callback(new Error('No snapshot named "' + snapname + '" for '
10737 + uuid));
10738 return;
10739 }
10740
10741 snapshot_list = [vmobj.zfs_filesystem + '@vmsnap-' + snapname];
10742
10743 if (vmobj.state !== 'stopped') {
10744 VM.stop(vmobj.uuid, {'force': true, log: log}, function (stop_err) {
10745 if (stop_err) {
10746 log.error(stop_err, 'failed to stop VM ' + vmobj.uuid
10747 + ': ' + stop_err.message);
10748 callback(stop_err);
10749 return;
10750 }
10751 performSnapshotRollback(snapshot_list, log,
10752 function (rollback_err) {
10753
10754 if (rollback_err) {
10755 log.error(rollback_err, 'failed to '
10756 + 'performSnapshotRollback');
10757 callback(rollback_err);
10758 return;
10759 }
10760 if (options.do_not_start) {
10761 callback();
10762 } else {
10763 VM.start(vmobj.uuid, {}, {log: log}, callback);
10764 }
10765 return;
10766 });
10767 });
10768 } else {
10769 performSnapshotRollback(snapshot_list, log,
10770 function (rollback_err) {
10771
10772 if (rollback_err) {
10773 log.error(rollback_err, 'failed to '
10774 + 'performSnapshotRollback');
10775 callback(rollback_err);
10776 return;
10777 }
10778 if (options.do_not_start) {
10779 callback();
10780 } else {
10781 VM.start(vmobj.uuid, {}, {log: log}, callback);
10782 }
10783 return;
10784 });
10785 }
10786 });
10787 };
10788
10789 exports.delete_snapshot = function (uuid, snapname, options, callback)
10790 {
10791 var load_fields;
10792 var log;
10793
10794 // options is optional
10795 if (arguments.length === 3) {
10796 callback = arguments[2];
10797 options = {};
10798 }
10799
10800 ensureLogging(true);
10801 if (options.hasOwnProperty('log')) {
10802 log = options.log;
10803 } else {
10804 log = VM.log.child({action: 'delete_snapshot', vm: uuid});
10805 }
10806
10807 if (!validSnapshotName(snapname, log)) {
10808 callback(new Error('Invalid snapshot name'));
10809 return;
10810 }
10811
10812 load_fields = [
10813 'brand',
10814 'snapshots',
10815 'zfs_filesystem',
10816 'zonepath',
10817 'zonename'
10818 ];
10819
10820 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10821 var found;
10822 var mountpath;
10823 var mountpoint;
10824 var snap;
10825 var zoneroot;
10826
10827 if (err) {
10828 callback(err);
10829 return;
10830 }
10831
10832 if (vmobj.brand === 'kvm') {
10833 callback(new Error('snapshots for KVM VMs currently unsupported'));
10834 return;
10835 }
10836
10837 found = false;
10838 if (vmobj.hasOwnProperty('snapshots')) {
10839 for (snap in vmobj.snapshots) {
10840 if (vmobj.snapshots[snap].name === snapname) {
10841 found = true;
10842 break;
10843 }
10844 }
10845 }
10846 if (!found) {
10847 callback(new Error('No snapshot named "' + snapname + '" for '
10848 + uuid));
10849 return;
10850 }
10851
10852 zoneroot = vmobj.zonepath + '/root';
10853 mountpath = '/checkpoints/' + snapname;
10854 mountpoint = zoneroot + '/' + mountpath;
10855
10856 async.waterfall([
10857 function (cb) {
10858 // Ensure it's safe for us to be doing something in this dir
10859 try {
10860 assertSafeZonePath(zoneroot, mountpath,
10861 {type: 'dir', enoent_ok: true});
10862 } catch (e) {
10863 log.error(e, 'Unsafe mountpoint for checkpoints: '
10864 + e.message);
10865 cb(e);
10866 return;
10867 }
10868 cb();
10869 }, function (cb) {
10870 // umount snapshot
10871 var argv;
10872 var cmd = '/usr/sbin/umount';
10873
10874 argv = [mountpoint];
10875
10876 execFile(cmd, argv, function (e, stdout, stderr) {
10877 if (e) {
10878 log.error({err: e}, 'There was an error while '
10879 + 'unmounting the snapshot: ' + e.message);
10880 // we treat an error here as fatal only if the error
10881 // was something other than 'not mounted'
10882 if (!stderr.match(/ not mounted/)) {
10883 cb(e);
10884 return;
10885 }
10886 } else {
10887 log.trace('umounted ' + mountpoint);
10888 }
10889 cb();
10890 });
10891 }, function (cb) {
10892 // remove the mountpoint directory
10893 fs.rmdir(mountpoint, function (e) {
10894 if (e) {
10895 log.error(e);
10896 } else {
10897 log.trace('removed directory ' + mountpoint);
10898 }
10899 cb(); // XXX not fatal because might also not exist
10900 });
10901 }, function (cb) {
10902 var args;
10903
10904 args = ['destroy', vmobj.zfs_filesystem + '@vmsnap-'
10905 + snapname];
10906
10907 zfs(args, log, function (e, fds) {
10908 if (e) {
10909 log.error({'err': e, 'stdout': fds.stdout,
10910 'stderr': fds.stdout}, 'zfs destroy failed.');
10911 cb(e);
10912 return;
10913 }
10914 log.debug({err: e, stdout: fds.stdout, stderr: fds.stderr},
10915 'zfs destroy ' + vmobj.zfs_filesystem + '@vmsnap-'
10916 + snapname);
10917 cb();
10918 });
10919 }, function (cb) {
10920 updateZonecfgTimestamp(vmobj, function (e) {
10921 if (e) {
10922 log.warn(e, 'failed to update timestamp after deleting '
10923 + 'snapshot');
10924 }
10925 // don't pass err because there's no recovery possible
10926 // (the snapshot's gone)
10927 cb();
10928 });
10929 }
10930 ], function (error) {
10931 callback(error);
10932 });
10933 });
10934 };
10935
10936 exports.create_snapshot = function (uuid, snapname, options, callback)
10937 {
10938 var load_fields;
10939 var log;
10940
10941 // options is optional
10942 if (arguments.length === 3) {
10943 callback = arguments[2];
10944 options = {};
10945 }
10946
10947 ensureLogging(true);
10948
10949 if (options.hasOwnProperty('log')) {
10950 log = options.log;
10951 } else {
10952 log = VM.log.child({action: 'create_snapshot', vm: uuid});
10953 }
10954
10955 if (!validSnapshotName(snapname, log)) {
10956 callback(new Error('Invalid snapshot name'));
10957 return;
10958 }
10959
10960 load_fields = [
10961 'brand',
10962 'datasets',
10963 'zone_state',
10964 'snapshots',
10965 'zfs_filesystem',
10966 'zonepath',
10967 'zonename'
10968 ];
10969
10970 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10971 var full_snapname;
10972 var mountpath;
10973 var mountpoint;
10974 var mount_snapshot = true;
10975 var snap;
10976 var snapshot_list = [];
10977 var zoneroot;
10978
10979 if (err) {
10980 callback(err);
10981 return;
10982 }
10983
10984 if (vmobj.brand === 'kvm') {
10985 callback(new Error('snapshots for KVM VMs currently unsupported'));
10986 return;
10987 }
10988
10989 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 0) {
10990 callback(new Error('Cannot currently snapshot zones that have '
10991 + 'datasets'));
10992 return;
10993 }
10994
10995 if (!vmobj.hasOwnProperty('zfs_filesystem')) {
10996 callback(new Error('vmobj missing zfs_filesystem, cannot create '
10997 + 'snapshot'));
10998 return;
10999 }
11000
11001 full_snapname = vmobj.zfs_filesystem + '@vmsnap-' + snapname;
11002
11003 // Check that name not already used
11004 if (vmobj.hasOwnProperty('snapshots')) {
11005 for (snap in vmobj.snapshots) {
11006 snap = vmobj.snapshots[snap];
11007
11008 if (snap.name === full_snapname) {
11009 callback(new Error('snapshot with name "' + snapname
11010 + '" already exists.'));
11011 return;
11012 } else {
11013 log.debug('SKIPPING ' + snap.name);
11014 }
11015 }
11016 }
11017
11018 snapshot_list.push(full_snapname);
11019
11020 // assert snapshot_list.length > 0
11021
11022 log.info('Taking snapshot "' + snapname + '" of ' + uuid);
11023
11024 zoneroot = vmobj.zonepath + '/root';
11025 mountpath = '/checkpoints/' + snapname;
11026 mountpoint = zoneroot + '/' + mountpath;
11027
11028 async.waterfall([
11029 function (cb) {
11030 // take the snapshot
11031 var args;
11032 args = ['snapshot'].concat(snapshot_list);
11033
11034 zfs(args, log, function (zfs_err, fds) {
11035 if (zfs_err) {
11036 log.error({err: zfs_err, stdout: fds.stdout,
11037 stderr: fds.stdout}, 'zfs snapshot failed.');
11038 } else {
11039 log.debug({err: zfs_err, stdout: fds.stdout,
11040 stderr: fds.stderr}, 'zfs ' + args.join(' '));
11041 }
11042 cb(zfs_err);
11043 });
11044 }, function (cb) {
11045
11046 if (vmobj.zone_state !== 'running') {
11047 log.info('Not mounting snapshot as zone is in state '
11048 + vmobj.zone_state + ', must be: running');
11049 mount_snapshot = false;
11050 cb();
11051 return;
11052 }
11053
11054 // Ensure it's safe for us to be doing something in this dir
11055 try {
11056 assertSafeZonePath(zoneroot, mountpath,
11057 {type: 'dir', enoent_ok: true});
11058 } catch (e) {
11059 log.error(e, 'Unsafe mountpoint for checkpoints: '
11060 + e.message);
11061 cb(e);
11062 return;
11063 }
11064 cb();
11065 }, function (cb) {
11066 // Make the mountpoint directory and parent
11067 var newmode;
11068
11069 if (mount_snapshot === false) {
11070 cb();
11071 return;
11072 }
11073
11074 /*jsl:ignore*/
11075 newmode = 0755;
11076 /*jsl:end*/
11077
11078 function doMkdir(dir, callbk) {
11079 fs.mkdir(dir, newmode, function (e) {
11080 if (e && e.code !== 'EEXIST') {
11081 log.error({err: e}, 'unable to create mountpoint '
11082 + 'for checkpoints: ' + e.message);
11083 callbk(e);
11084 return;
11085 }
11086 callbk();
11087 });
11088 }
11089
11090 doMkdir(path.dirname(mountpoint), function (parent_e) {
11091 if (parent_e) {
11092 cb(parent_e);
11093 return;
11094 }
11095 doMkdir(mountpoint, function (dir_e) {
11096 if (dir_e) {
11097 cb(dir_e);
11098 return;
11099 }
11100
11101 log.debug('created ' + mountpoint);
11102 cb();
11103 });
11104 });
11105 }, function (cb) {
11106 var argv;
11107 var cmd = '/usr/sbin/mount';
11108 var snapdir;
11109
11110 if (mount_snapshot === false) {
11111 cb();
11112 return;
11113 }
11114
11115 snapdir = vmobj.zonepath + '/.zfs/snapshot/vmsnap-' + snapname
11116 + '/root';
11117 argv = [ '-F', 'lofs', '-o', 'ro,setuid,nodevices', snapdir,
11118 mountpoint];
11119
11120 execFile(cmd, argv, function (e, stdout, stderr) {
11121 if (e) {
11122 log.error({err: e}, 'unable to mount snapshot: '
11123 + e.message);
11124 }
11125 // not fatal becase snapshot was already created.
11126 cb();
11127 });
11128 }, function (cb) {
11129 // update timestamp so last_modified gets bumped
11130 updateZonecfgTimestamp(vmobj, function (e) {
11131 if (e) {
11132 log.warn(e,
11133 'failed to update timestamp after snapshot');
11134 }
11135 // ignore error since there's no recovery
11136 // (snapshot was created)
11137 cb();
11138 });
11139 }
11140 ], function (error) {
11141 callback(error);
11142 });
11143 });
11144 };
11145
11146 exports.start = function (uuid, extra, options, callback)
11147 {
11148 var load_fields;
11149 var log;
11150
11151 load_fields = [
11152 'brand',
11153 'nics',
11154 'state',
11155 'uuid',
11156 'zone_state',
11157 'zonename',
11158 'zonepath'
11159 ];
11160
11161 // options is optional
11162 if (arguments.length === 3) {
11163 callback = arguments[2];
11164 options = {};
11165 }
11166
11167 assert(callback, 'undefined callback!');
11168
11169 ensureLogging(true);
11170 if (options.hasOwnProperty('log')) {
11171 log = options.log;
11172 } else {
11173 log = VM.log.child({action: 'start', vm: uuid});
11174 }
11175
11176 log.info('Starting VM ' + uuid);
11177
11178 VM.load(uuid, {log: log, fields: load_fields}, function (err, vmobj) {
11179 if (err) {
11180 callback(err);
11181 } else {
11182
11183 if (vmobj.state === 'running') {
11184 err = new Error('VM ' + vmobj.uuid + ' is already '
11185 + '\'running\'');
11186 err.code = 'EALREADYRUNNING';
11187 callback(err);
11188 return;
11189 }
11190
11191 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning')
11192 || (vmobj.state === 'provisioning'
11193 && vmobj.zone_state !== 'installed')) {
11194
11195 err = new Error('Cannot to start vm from state "' + vmobj.state
11196 + '", must be "stopped".');
11197 log.error(err);
11198 callback(err);
11199 return;
11200 }
11201
11202 lookupInvalidNicTags(vmobj.nics, log, function (e) {
11203 var kvm_load_fields = [
11204 'boot',
11205 'brand',
11206 'cpu_type',
11207 'default_gateway',
11208 'disks',
11209 'hostname',
11210 'internal_metadata',
11211 'never_booted',
11212 'nics',
11213 'qemu_extra_opts',
11214 'qemu_opts',
11215 'ram',
11216 'resolvers',
11217 'spice_opts',
11218 'spice_password',
11219 'spice_port',
11220 'state',
11221 'uuid',
11222 'vcpus',
11223 'vga',
11224 'virtio_txtimer',
11225 'virtio_txburst',
11226 'vnc_password',
11227 'zone_state',
11228 'zonename',
11229 'zonepath'
11230 ];
11231
11232 if (e) {
11233 callback(e);
11234 return;
11235 }
11236
11237 if (BRAND_OPTIONS[vmobj.brand].features.type === 'KVM') {
11238 // when we boot KVM we need a lot more fields, so load again
11239 // in that case to get the fields we need.
11240 VM.load(uuid, {log: log, fields: kvm_load_fields},
11241 function (error, obj) {
11242
11243 if (error) {
11244 callback(error);
11245 return;
11246 }
11247 startVM(obj, extra, log, callback);
11248 });
11249 } else if (BRAND_OPTIONS[vmobj.brand].features.type === 'OS') {
11250 startZone(vmobj, log, callback);
11251 } else {
11252 err = new Error('no idea how to start a vm with brand: '
11253 + vmobj.brand);
11254 log.error(err);
11255 callback(err);
11256 }
11257 });
11258 }
11259 });
11260 };
11261
11262 function setRctl(zonename, rctl, value, log, callback)
11263 {
11264 var args;
11265
11266 assert(log, 'no logger passed to setRctl()');
11267
11268 args = ['-n', rctl, '-v', value.toString(), '-r', '-i', 'zone', zonename];
11269 log.debug('/usr/bin/prctl ' + args.join(' '));
11270 execFile('/usr/bin/prctl', args, function (error, stdout, stderr) {
11271 if (error) {
11272 log.error(error, 'setRctl() failed with: ' + stderr);
11273 callback(error);
11274 } else {
11275 callback();
11276 }
11277 });
11278 }
11279
11280 function resizeTmp(zonename, newsize, log, callback)
11281 {
11282 var args;
11283
11284 // NOTE: this used to update /etc/vfstab in the zone as well, but was
11285 // changed with OS-920. Now vfstab is updated by mdata-fetch in the
11286 // zone instead, so that will happen next boot. We still do the mount
11287 // so the property update happens on the running zone.
11288
11289 assert(log, 'no logger passed to resizeTmp()');
11290
11291 args = [zonename, '/usr/sbin/mount', '-F', 'tmpfs', '-o', 'remount,size='
11292 + newsize + 'm', '/tmp'];
11293 log.debug('/usr/sbin/zlogin ' + args.join(' '));
11294 execFile('/usr/sbin/zlogin', args, function (err, mnt_stdout, mnt_stderr) {
11295 if (err) {
11296 log.error({'err': err, 'stdout': mnt_stdout,
11297 'stderr': mnt_stderr}, 'zlogin for ' + zonename
11298 + ' exited with code ' + err.code + ' -- ' + err.message);
11299 // error here is not fatal as this should be fixed on reboot
11300 }
11301
11302 callback();
11303 });
11304 }
11305
11306 function resizeDisks(disks, updates, log, callback)
11307 {
11308 var d;
11309 var disk;
11310 var resized = 0;
11311 var vols = [];
11312
11313 assert(log, 'no logger passed to resizeDisks()');
11314
11315 for (disk in updates) {
11316 disk = updates[disk];
11317 for (d in disks) {
11318 d = disks[d];
11319 if (d.path === disk.path && disk.hasOwnProperty('size')) {
11320 vols.push({'disk': d, 'new_size': disk.size});
11321 }
11322 }
11323 }
11324
11325 function resize(vol, cb) {
11326 var args;
11327 var dsk = vol.disk;
11328 var size = vol.new_size;
11329
11330 if (dsk.hasOwnProperty('zfs_filesystem')) {
11331 if (dsk.size > size) {
11332 cb(new Error('cannot resize ' + dsk.zfs_filesystem
11333 + ' new size must be greater than current size. ('
11334 + dsk.size + ' > ' + dsk.size + ')'));
11335 } else if (dsk.size === size) {
11336 // no point resizing if the old+new are the same
11337 cb();
11338 } else {
11339 args = ['set', 'volsize=' + size + 'M', dsk.zfs_filesystem];
11340 zfs(args, log, function (err, fds) {
11341 resized++;
11342 cb(err);
11343 });
11344 }
11345 } else {
11346 cb(new Error('could not find zfs_filesystem in '
11347 + JSON.stringify(dsk)));
11348 }
11349 }
11350
11351 async.forEachSeries(vols, resize, function (err) {
11352 if (err) {
11353 log.error(err, 'Unable to resize disks');
11354 callback(err);
11355 } else {
11356 callback(null, resized);
11357 }
11358 });
11359 }
11360
11361 function updateVnicAllowedIPs(uuid, nic, log, callback)
11362 {
11363 var ips = [];
11364
11365 assert(log, 'no logger passed to updateVnicAllowedIPs()');
11366
11367 if (!uuid || !nic.interface) {
11368 callback();
11369 return;
11370 }
11371
11372 if (nic.hasOwnProperty('allow_ip_spoofing') && nic.allow_ip_spoofing) {
11373 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback);
11374 return;
11375 }
11376
11377 if (nic.hasOwnProperty('ip')) {
11378 ips.push(nic.ip);
11379 }
11380
11381 if (nic.hasOwnProperty('vrrp_primary_ip')) {
11382 ips.push(nic.vrrp_primary_ip);
11383 }
11384
11385 if (nic.hasOwnProperty('allowed_ips')) {
11386 ips = ips.concat(nic.allowed_ips);
11387 }
11388
11389 if (!ips.length === 0) {
11390 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback);
11391 } else {
11392 dladm.setLinkProp(uuid, nic.interface, 'allowed-ips', ips, log,
11393 callback);
11394 }
11395 }
11396
11397 function updateVnicProperties(uuid, vmobj, payload, log, callback)
11398 {
11399 assert(log, 'no logger passed to updateVnicProperties()');
11400
11401 if (vmobj.state != 'running') {
11402 log.debug('VM not running: not updating vnic properties');
11403 callback(null);
11404 return;
11405 }
11406
11407 if (!payload.hasOwnProperty('update_nics')) {
11408 log.debug(
11409 'No update_nics property: not updating vnic properties');
11410 callback(null);
11411 return;
11412 }
11413
11414 async.forEach(payload.update_nics, function (nic, cb) {
11415 var opt;
11416 var needsUpdate = false;
11417 var needsIPupdate = false;
11418 var spoof_opts = {
11419 'allow_ip_spoofing': 'ip-nospoof',
11420 'allow_mac_spoofing': 'mac-nospoof',
11421 'allow_dhcp_spoofing': 'dhcp-nospoof',
11422 'allow_restricted_traffic': 'restricted'
11423 };
11424 var vm_nic;
11425
11426 // First, determine if we've changed any of the spoofing opts in this
11427 // update:
11428 for (opt in spoof_opts) {
11429 if (nic.hasOwnProperty(opt)) {
11430 needsUpdate = true;
11431 break;
11432 }
11433 }
11434
11435 if (nic.hasOwnProperty('vrrp_primary_ip')
11436 || nic.hasOwnProperty('allowed_ips')
11437 || nic.hasOwnProperty('allow_ip_spoofing')) {
11438 needsIPupdate = true;
11439 }
11440
11441 for (vm_nic in vmobj.nics) {
11442 vm_nic = vmobj.nics[vm_nic];
11443 if (vm_nic.mac == nic.mac) {
11444 break;
11445 }
11446 }
11447
11448 if (!vm_nic) {
11449 cb(new Error('Unknown NIC: ' + nic.mac));
11450 return;
11451 }
11452
11453 if (!needsUpdate) {
11454 log.debug('No spoofing / allowed IP opts updated for nic "'
11455 + nic.mac + '": not updating');
11456 if (needsIPupdate) {
11457 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11458 } else {
11459 cb(null);
11460 }
11461 return;
11462 }
11463
11464 // Using the updated nic object, figure out what spoofing opts to set
11465 for (opt in spoof_opts) {
11466 if (vm_nic.hasOwnProperty(opt) && fixBoolean(vm_nic[opt])) {
11467 delete spoof_opts[opt];
11468 }
11469 }
11470
11471 if (vm_nic.hasOwnProperty('dhcp_server')
11472 && fixBoolean(vm_nic.dhcp_server)) {
11473 delete spoof_opts.allow_dhcp_spoofing;
11474 delete spoof_opts.allow_ip_spoofing;
11475 }
11476
11477 if (Object.keys(spoof_opts).length === 0) {
11478 dladm.resetLinkProp(uuid, vm_nic.interface, 'protection', log,
11479 function (err) {
11480 if (err) {
11481 cb(err);
11482 return;
11483 }
11484 if (needsIPupdate) {
11485 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11486 return;
11487 }
11488 cb();
11489 return;
11490 });
11491 } else {
11492 dladm.setLinkProp(uuid, vm_nic.interface, 'protection',
11493 Object.keys(spoof_opts).map(function (k) {
11494 return spoof_opts[k];
11495 }), log,
11496 function (err) {
11497 if (err) {
11498 cb(err);
11499 return;
11500 }
11501 if (needsIPupdate) {
11502 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11503 return;
11504 }
11505 cb();
11506 return;
11507 });
11508 }
11509 }, function (err) {
11510 if (err) {
11511 callback(err);
11512 } else {
11513 callback(null);
11514 }
11515 });
11516 }
11517
11518 // Run a fw.js function that requires all VM records
11519 function firewallVMrun(opts, fn, log, callback)
11520 {
11521 assert(log, 'no logger passed to firewallVMrun()');
11522 VM.lookup({}, {fields: fw.VM_FIELDS, log: log}, function (err, records) {
11523 if (err) {
11524 callback(err);
11525 return;
11526 }
11527
11528 opts.vms = records;
11529 if (fn.name == 'validatePayload') {
11530 opts.logName = 'VM-create';
11531 } else {
11532 opts.logName = 'VM-' + (fn.name || '');
11533 }
11534
11535 if (opts.provisioning) {
11536 opts.vms.push(opts.provisioning);
11537 delete opts.provisioning;
11538 }
11539
11540 fn(opts, callback);
11541 return;
11542 });
11543 }
11544
11545 function validateFirewall(payload, log, callback)
11546 {
11547 assert(log, 'no logger passed to validateFirewall()');
11548
11549 log.debug(toValidate, 'Validating firewall payload');
11550 var toValidate = payload.firewall;
11551 toValidate.provisioning = {
11552 'state': 'provisioning'
11553 };
11554
11555 fw.VM_FIELDS.forEach(function (field) {
11556 if (payload.hasOwnProperty(field)) {
11557 toValidate.provisioning[field] = payload[field];
11558 }
11559 });
11560
11561 if (payload.hasOwnProperty('add_nics')) {
11562 toValidate.provisioning.nics = payload.add_nics;
11563 }
11564
11565 // We're not actually writing data to zonepath when validating, and we
11566 // don't actually have a zonepath created yet, so add a key so that the
11567 // payload passes validation
11568 if (!payload.hasOwnProperty('zonepath')) {
11569 toValidate.provisioning.zonepath = true;
11570 }
11571
11572 log.debug({
11573 firewall: toValidate.firewall,
11574 provisioning: toValidate.provisioning,
11575 payload: payload
11576 }, 'Validating firewall payload');
11577
11578 firewallVMrun(toValidate, fw.validatePayload, log,
11579 function (err, res) {
11580 if (err) {
11581 log.error(err, 'Error validating firewall payload');
11582 err.message = 'Invalid firewall payload: ' + err.message;
11583 }
11584
11585 callback(err, res);
11586 return;
11587 });
11588 }
11589
11590 function addFirewallData(payload, vmobj, log, callback)
11591 {
11592 var firewallOpts = payload.firewall;
11593
11594 assert(log, 'no logger passed to addFirewallData()');
11595
11596 if (!payload.hasOwnProperty('firewall')) {
11597 firewallOpts = {};
11598 }
11599 firewallOpts.localVMs = [vmobj];
11600
11601 log.debug(firewallOpts, 'Adding firewall data');
11602 firewallVMrun(firewallOpts, fw.add, log, function (err, res) {
11603 if (err) {
11604 log.error(err, 'Error adding firewall data');
11605 }
11606
11607 callback(err, res);
11608 return;
11609 });
11610 }
11611
11612 function updateFirewallData(payload, vmobj, log, callback)
11613 {
11614 var enablePrefix = 'En';
11615 var enableFn = fw.enable;
11616 var firewallOpts = payload.firewall;
11617
11618 assert(log, 'no logger passed to updateFirewallData()');
11619
11620 if (!payload.hasOwnProperty('firewall')) {
11621 firewallOpts = {};
11622 }
11623 firewallOpts.localVMs = [vmobj];
11624
11625 log.debug(firewallOpts, 'Updating firewall data');
11626 firewallVMrun(firewallOpts, fw.update, log, function (err, res) {
11627 if (err) {
11628 log.error(err, 'Error updating firewall data');
11629 }
11630
11631 if (!payload.hasOwnProperty('firewall_enabled')) {
11632 callback(err, res);
11633 return;
11634 }
11635
11636 if (!payload.firewall_enabled) {
11637 enableFn = fw.disable;
11638 enablePrefix = 'Dis';
11639 }
11640
11641 log.debug('%sabling firewall for VM %s', enablePrefix, vmobj.uuid);
11642 firewallVMrun({ vm: vmobj }, enableFn, log, function (err2, res2) {
11643 if (err2) {
11644 log.error(err, 'Error %sabling firewall',
11645 enablePrefix.toLowerCase());
11646 }
11647
11648 callback(err2, res2);
11649 return;
11650 });
11651 });
11652 }
11653
11654 function restartMetadataService(vmobj, payload, log, callback) {
11655 var args;
11656
11657 assert(log, 'no logger passed to restartMetadataService()');
11658
11659 if (!BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
11660 || !BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
11661 || !BRAND_OPTIONS[vmobj.brand].features.mdata_restart) {
11662 log.debug('restarting mdata:fetch service not supported for brand '
11663 + vmobj.brand);
11664 callback();
11665 return;
11666 }
11667
11668 if (vmobj.state !== 'running' || !payload.hasOwnProperty('resolvers')
11669 && !payload.hasOwnProperty('routes')
11670 && !payload.hasOwnProperty('set_routes')
11671 && !payload.hasOwnProperty('remove_routes')) {
11672 callback();
11673 return;
11674 }
11675
11676 log.debug('restarting metadata service for: ' + vmobj.uuid);
11677
11678 args = [vmobj.zonename, '/usr/sbin/svcadm', 'restart',
11679 'svc:/smartdc/mdata:fetch'];
11680 log.debug('/usr/sbin/zlogin ' + args.join(' '));
11681 execFile('/usr/sbin/zlogin', args, function (err, svc_stdout, svc_stderr) {
11682 if (err) {
11683 log.error({'err': err, 'stdout': svc_stdout,
11684 'stderr': svc_stderr}, 'zlogin for ' + vmobj.zonename
11685 + ' exited with code' + err.code + err.message);
11686 // error here is not fatal as this should be fixed on reboot
11687 }
11688
11689 callback();
11690 });
11691 }
11692
11693 function applyUpdates(oldobj, newobj, payload, log, callback)
11694 {
11695 var changed_datasets = false;
11696
11697 assert(log, 'no logger passed to applyUpdates()');
11698
11699 // Note: oldobj is the VM *before* the update, newobj *after*
11700 log.debug('applying updates to ' + oldobj.uuid);
11701
11702 async.series([
11703 function (cb) {
11704 if (payload.hasOwnProperty('update_disks')
11705 && oldobj.hasOwnProperty('disks')) {
11706
11707 resizeDisks(oldobj.disks, payload.update_disks, log,
11708 function (err, resized) {
11709 // If any were resized, mark that we changed something
11710 if (!err && resized > 0) {
11711 changed_datasets = true;
11712 }
11713 cb(err);
11714 }
11715 );
11716 } else {
11717 cb();
11718 }
11719 }, function (cb) {
11720 if (payload.hasOwnProperty('quota')
11721 && (Number(payload.quota) !== Number(oldobj.quota))) {
11722
11723 setQuota(newobj.zfs_filesystem, payload.quota, log,
11724 function (err) {
11725
11726 if (!err) {
11727 changed_datasets = true;
11728 }
11729 cb(err);
11730 });
11731 } else {
11732 cb();
11733 }
11734 }, function (cb) {
11735 // NOTE: we've already validated the value
11736 if (payload.hasOwnProperty('zfs_root_recsize')
11737 && (payload.zfs_root_recsize !== oldobj.zfs_root_recsize)) {
11738
11739 zfs(['set', 'recsize=' + payload.zfs_root_recsize,
11740 newobj.zfs_filesystem], log, function (err, fds) {
11741
11742 if (err) {
11743 log.error(err, 'failed to apply zfs_root_recsize: '
11744 + fds.stderr);
11745 cb(new Error(rtrim(fds.stderr)));
11746 } else {
11747 cb();
11748 }
11749 });
11750 } else {
11751 cb();
11752 }
11753 }, function (cb) {
11754 // NOTE: we've already validated the value.
11755 if (payload.hasOwnProperty('zfs_data_recsize')
11756 && oldobj.hasOwnProperty('zfs_data_recsize')
11757 && newobj.hasOwnProperty('datasets')
11758 && (newobj.datasets.indexOf(newobj.zfs_filesystem
11759 + '/data') !== -1)) {
11760
11761 zfs(['set', 'recsize=' + payload.zfs_data_recsize,
11762 newobj.zfs_filesystem + '/data'], log, function (err, fds) {
11763
11764 if (err) {
11765 log.error(err, 'failed to apply zfs_data_recsize: '
11766 + fds.stderr);
11767 cb(new Error(rtrim(fds.stderr)));
11768 } else {
11769 cb();
11770 }
11771 });
11772 } else {
11773 cb();
11774 }
11775 }, function (cb) {
11776 // NOTE: we've already validated the value
11777 if (payload.hasOwnProperty('zfs_root_compression')
11778 && (payload.zfs_root_compression !==
11779 oldobj.zfs_root_compression)) {
11780
11781 zfs(['set', 'compression=' + payload.zfs_root_compression,
11782 newobj.zfs_filesystem], log, function (err, fds) {
11783
11784 if (err) {
11785 log.error(err, 'failed to apply '
11786 + 'zfs_root_compression: ' + fds.stderr);
11787 cb(new Error(rtrim(fds.stderr)));
11788 } else {
11789 cb();
11790 }
11791 });
11792 } else {
11793 cb();
11794 }
11795 }, function (cb) {
11796 // NOTE: we've already validated the value
11797 if (payload.hasOwnProperty('zfs_data_compression')
11798 && newobj.hasOwnProperty('datasets')
11799 && (newobj.datasets.indexOf(newobj.zfs_filesystem
11800 + '/data') !== -1)) {
11801
11802 zfs(['set', 'compression=' + payload.zfs_data_compression,
11803 newobj.zfs_filesystem + '/data'], log, function (err, fds) {
11804
11805 if (err) {
11806 log.error(err, 'failed to apply '
11807 + 'zfs_data_compression: ' + fds.stderr);
11808 cb(new Error(rtrim(fds.stderr)));
11809 } else {
11810 cb();
11811 }
11812 });
11813 } else {
11814 cb();
11815 }
11816 }, function (cb) {
11817 var d;
11818 var disk;
11819 var zfs_updates = [];
11820
11821 if (payload.hasOwnProperty('update_disks')) {
11822 // loop through the disks we updated and perform any updates.
11823 for (disk in payload.update_disks) {
11824 disk = payload.update_disks[disk];
11825
11826 if (!disk) {
11827 continue;
11828 }
11829
11830 for (d in oldobj.disks) {
11831 d = oldobj.disks[d];
11832 if (d.path === disk.path
11833 && d.hasOwnProperty('zfs_filesystem')) {
11834
11835 if (disk.hasOwnProperty('compression')) {
11836 zfs_updates.push({
11837 zfs_filesystem: d.zfs_filesystem,
11838 property: 'compression',
11839 value: disk.compression
11840 });
11841 }
11842
11843 if (disk.hasOwnProperty('refreservation')) {
11844 zfs_updates.push({
11845 zfs_filesystem: d.zfs_filesystem,
11846 property: 'refreservation',
11847 value: disk.refreservation + 'M'
11848 });
11849 }
11850 }
11851 }
11852 }
11853 if (zfs_updates.length > 0) {
11854 log.debug('applying ' + zfs_updates.length
11855 + ' zfs updates');
11856 async.each(zfs_updates, function (props, f_cb) {
11857 zfs(['set', props.property + '=' + props.value,
11858 props.zfs_filesystem], log, function (err, fds) {
11859
11860 if (err) {
11861 log.error(err, 'failed to set ' + props.property
11862 + '=' + props.value + ' for '
11863 + props.zfs_filesystem);
11864 }
11865 f_cb(err);
11866 });
11867 }, function (err) {
11868 log.debug({err: err}, 'end of zfs updates');
11869 cb(err);
11870 });
11871 } else {
11872 log.debug('no zfs updates to apply');
11873 cb();
11874 }
11875 } else {
11876 cb();
11877 }
11878 }, function (cb) {
11879 var factor;
11880 var keys = [];
11881 var rctl;
11882 var rctls = {
11883 'cpu_shares': ['zone.cpu-shares'],
11884 'zfs_io_priority': ['zone.zfs-io-priority'],
11885 'max_lwps': ['zone.max-lwps'],
11886 'max_physical_memory': ['zone.max-physical-memory',
11887 (1024 * 1024)],
11888 'max_locked_memory': ['zone.max-locked-memory', (1024 * 1024)],
11889 'max_swap': ['zone.max-swap', (1024 * 1024)],
11890 'cpu_cap': ['zone.cpu-cap']
11891 };
11892
11893 if (!BRAND_OPTIONS[oldobj.brand].features.update_rctls) {
11894 cb();
11895 return;
11896 }
11897
11898 for (rctl in rctls) {
11899 keys.push(rctl);
11900 }
11901
11902 async.forEachSeries(keys, function (prop, c) {
11903 rctl = rctls[prop][0];
11904 if (rctls[prop][1]) {
11905 factor = rctls[prop][1];
11906 } else {
11907 factor = 1;
11908 }
11909
11910 if (payload.hasOwnProperty(prop)) {
11911 setRctl(newobj.zonename, rctl,
11912 Number(payload[prop]) * factor, log,
11913 function (err) {
11914 if (err) {
11915 log.warn(err, 'failed to set rctl: ' + prop);
11916 }
11917 c();
11918 }
11919 );
11920 } else {
11921 c();
11922 }
11923 }, function (err) {
11924 cb(err);
11925 });
11926 }, function (cb) {
11927 if ((payload.hasOwnProperty('vnc_password')
11928 && (oldobj.vnc_password !== newobj.vnc_password))
11929 || (payload.hasOwnProperty('vnc_port')
11930 && (oldobj.vnc_port !== newobj.vnc_port))) {
11931
11932 // tell vmadmd to refresh_password and port (will restart
11933 // listener)
11934 postVmadmd(newobj.uuid, 'reload_display', {}, log,
11935 function (e) {
11936
11937 if (e) {
11938 cb(new Error('Unable to tell vmadmd to reload VNC: '
11939 + e.message));
11940 } else {
11941 cb();
11942 }
11943 });
11944 } else if ((payload.hasOwnProperty('spice_password')
11945 && (oldobj.spice_password !== newobj.spice_password))
11946 || (payload.hasOwnProperty('spice_port')
11947 && (oldobj.spice_port !== newobj.spice_port))) {
11948
11949 // tell vmadmd to refresh_password and port (will restart
11950 // listener)
11951 postVmadmd(newobj.uuid, 'reload_display', {}, log,
11952 function (e) {
11953
11954 if (e) {
11955 cb(new Error('Unable to tell vmadmd to reload SPICE: '
11956 + e.message));
11957 } else {
11958 cb();
11959 }
11960 });
11961 } else {
11962 cb();
11963 }
11964 }, function (cb) {
11965 // we do this last, since we need the memory in the zone updated
11966 // first if we're growing this.
11967 if (payload.hasOwnProperty('tmpfs')) {
11968 resizeTmp(newobj.zonename, payload.tmpfs, log, cb);
11969 } else {
11970 cb();
11971 }
11972 }, function (cb) {
11973 var now = new Date();
11974
11975 // If we changed any dataset properties, we touch the zone's xml
11976 // file so that last_modified is correct.
11977 if (changed_datasets && newobj.hasOwnProperty('zonename')) {
11978 fs.utimes('/etc/zones/' + newobj.zonename + '.xml', now, now,
11979 function (err) {
11980 if (err) {
11981 log.warn(err, 'Unable to "touch" xml file for "'
11982 + newobj.zonename + '": ' + err.message);
11983 } else {
11984 log.debug('Touched ' + newobj.zonename
11985 + '.xml after datasets were modified.');
11986 }
11987 // We don't error out if we just couldn't touch because
11988 // the actual updates above already did happen.
11989 cb();
11990 }
11991 );
11992 } else {
11993 cb();
11994 }
11995 }
11996
11997 ], function (err, res) {
11998 log.debug('done applying updates to ' + oldobj.uuid);
11999 callback(err);
12000 });
12001 }
12002
12003 exports.update = function (uuid, payload, options, callback)
12004 {
12005 var log;
12006 var new_vmobj;
12007 var vmobj;
12008
12009 // options parameter is optional
12010 if (arguments.length === 3) {
12011 callback = arguments[2];
12012 options = {};
12013 }
12014
12015 ensureLogging(true);
12016 if (options.hasOwnProperty('log')) {
12017 log = options.log;
12018 } else {
12019 log = VM.log.child({action: 'update', vm: uuid});
12020 }
12021
12022 log.info('Updating VM ' + uuid + ' with initial payload:\n'
12023 + JSON.stringify(payload, null, 2));
12024
12025 async.series([
12026 function (cb) {
12027 // for update we currently always load the whole vmobj since the
12028 // update functions may need to look at bits from the existing VM.
12029 VM.load(uuid, {log: log}, function (err, obj) {
12030 if (err) {
12031 cb(err);
12032 return;
12033 }
12034 vmobj = obj;
12035 cb();
12036 });
12037 }, function (cb) {
12038 normalizePayload(payload, vmobj, log, function (e) {
12039 log.debug('Used payload:\n'
12040 + JSON.stringify(payload, null, 2));
12041 cb(e);
12042 });
12043 }, function (cb) {
12044 var deletables = [];
12045 var to_remove = [];
12046 var n;
12047
12048 // destroy remove_disks before we add in case we're recreating with
12049 // an existing name.
12050
12051 if (payload.hasOwnProperty('remove_disks')) {
12052 to_remove = payload.remove_disks;
12053 for (n in vmobj.disks) {
12054 if (to_remove.indexOf(vmobj.disks[n].path) !== -1) {
12055 deletables.push(vmobj.disks[n]);
12056 }
12057 }
12058 } else {
12059 // no disks to remove so all done.
12060 cb();
12061 return;
12062 }
12063
12064 function loggedDeleteVolume(volume, callbk) {
12065 return deleteVolume(volume, log, callbk);
12066 }
12067
12068 async.forEachSeries(deletables, loggedDeleteVolume,
12069 function (err) {
12070 if (err) {
12071 log.error(err, 'Unknown error deleting volumes: '
12072 + err.message);
12073 cb(err);
12074 } else {
12075 log.info('successfully deleted volumes');
12076 cb();
12077 }
12078 }
12079 );
12080 }, function (cb) {
12081 var disks = [];
12082 var matches;
12083 var n;
12084 var p;
12085 var used_disk_indexes = [];
12086
12087 // create any new volumes we need.
12088 if (payload.hasOwnProperty('add_disks')) {
12089 disks = payload.add_disks;
12090 }
12091
12092 // create a list of used indexes so we can find the free ones to
12093 // use in createVolume()
12094 if (vmobj.hasOwnProperty('disks')) {
12095 for (n in vmobj.disks) {
12096 matches = vmobj.disks[n].path.match(/^.*-disk(\d+)$/);
12097 if (matches) {
12098 used_disk_indexes.push(Number(matches[1]));
12099 }
12100 }
12101 }
12102
12103 // add the bits of payload createVolumes() needs.
12104 p = {'add_disks': disks};
12105 p.uuid = uuid;
12106 if (vmobj.hasOwnProperty('zpool')) {
12107 p.zpool = vmobj.zpool;
12108 }
12109 p.used_disk_indexes = used_disk_indexes;
12110 createVolumes(p, log, function (e) {
12111 cb(e);
12112 });
12113 }, function (cb) {
12114 updateMetadata(vmobj, payload, log, function (e) {
12115 cb(e);
12116 });
12117 }, function (cb) {
12118 updateRoutes(vmobj, payload, log, function (e) {
12119 cb(e);
12120 });
12121 }, function (cb) {
12122 var zcfg;
12123 // generate a payload and send as a file to zonecfg to update
12124 // the zone.
12125 zcfg = buildZonecfgUpdate(vmobj, payload, log);
12126 zonecfgFile(zcfg, ['-u', uuid], log, function (e, fds) {
12127 if (e) {
12128 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr},
12129 'unable to update zonecfg');
12130 } else {
12131 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12132 'updated zonecfg');
12133 }
12134 cb(e);
12135 });
12136 }, function (cb) {
12137 restartMetadataService(vmobj, payload, log, function (e) {
12138 cb(e);
12139 });
12140 }, function (cb) {
12141 updateVnicProperties(uuid, vmobj, payload, log, function (e) {
12142 cb(e);
12143 });
12144 }, function (cb) {
12145 // Update the firewall data
12146 updateFirewallData(payload, vmobj, log, cb);
12147 }, function (cb) {
12148 // Do another full reload (all fields) so we can compare in
12149 // applyUpdates() and decide what's changed that we need to apply.
12150 VM.load(uuid, {log: log}, function (e, newobj) {
12151 if (e) {
12152 cb(e);
12153 } else {
12154 new_vmobj = newobj;
12155 cb();
12156 }
12157 });
12158 }, function (cb) {
12159 applyUpdates(vmobj, new_vmobj, payload, log, function () {
12160 cb();
12161 });
12162 }
12163 ], function (e) {
12164 callback(e);
12165 });
12166 };
12167
12168 function kill(uuid, log, callback)
12169 {
12170 var load_fields;
12171 var unset_autoboot = 'set autoboot=false';
12172
12173 assert(log, 'no logger passed to kill()');
12174
12175 log.info('Killing VM ' + uuid);
12176
12177 load_fields = [
12178 'brand',
12179 'state',
12180 'transition_to',
12181 'uuid'
12182 ];
12183
12184 /* We load here to ensure this vm exists. */
12185 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12186 if (err) {
12187 callback(err);
12188 return;
12189 }
12190
12191 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12192 unset_autoboot =
12193 'select attr name=vm-autoboot; set value=false; end';
12194 }
12195
12196 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) {
12197 var msg = trim(fds.stderr);
12198
12199 if (msg.match(/zone is already halted$/)) {
12200 // remove transition marker, since vm is not running now.
12201 VM.unsetTransition(vmobj, {log: log}, function () {
12202 var new_err;
12203
12204 new_err = new Error('VM ' + vmobj.uuid + ' is already '
12205 + 'not \'running\' (currently: ' + vmobj.state + ')');
12206 new_err.code = 'ENOTRUNNING';
12207 callback(new_err);
12208 });
12209 } else if (e) {
12210 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr},
12211 'failed to halt VM ' + uuid);
12212 callback(err, msg);
12213 } else {
12214 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12215 'zoneadm halted VM ' + uuid);
12216 zonecfg(['-u', uuid, unset_autoboot], log,
12217 function (error, unset_fds) {
12218
12219 if (error) {
12220 // The vm is dead at this point, erroring out here would
12221 // do no good, so we just log it.
12222 log.error({err: error, stdout: unset_fds.stdout,
12223 stderr: unset_fds.stderr}, 'killVM(): Failed to '
12224 + unset_autoboot);
12225 } else {
12226 log.debug({stdout: unset_fds.stdout,
12227 stderr: unset_fds.stderr}, 'unset autoboot flag');
12228 }
12229 if (vmobj.state === 'stopping') {
12230 // remove transition marker
12231 VM.unsetTransition(vmobj, {log: log}, function () {
12232 callback(null, msg);
12233 });
12234 } else {
12235 callback(null, msg);
12236 }
12237 });
12238 }
12239 });
12240 });
12241 }
12242
12243 function postVmadmd(uuid, action, args, log, callback)
12244 {
12245 var arg;
12246 var url_path = '/vm/' + uuid + '?action=' + action;
12247 var req;
12248
12249 assert(log, 'no logger passed to postVmadmd()');
12250
12251 if (args) {
12252 for (arg in args) {
12253 if (args.hasOwnProperty(arg)) {
12254 url_path = url_path + '&' + arg + '=' + args[arg];
12255 }
12256 }
12257 }
12258
12259 log.debug('HTTP POST ' + url_path);
12260 req = http.request(
12261 { method: 'POST', host: '127.0.0.1', port: '8080', path: url_path },
12262 function (res) {
12263
12264 log.debug('HTTP STATUS: ' + res.statusCode);
12265 log.debug('HTTP HEADERS: ' + JSON.stringify(res.headers));
12266 res.setEncoding('utf8');
12267 res.on('data', function (chunk) {
12268 log.debug('HTTP BODY: ' + chunk);
12269 });
12270 res.on('end', function () {
12271 log.debug('HTTP conversation has completed.');
12272 callback();
12273 });
12274 }
12275 );
12276 req.on('error', function (e) {
12277 log.error(e, 'HTTP error: ' + e.message);
12278 callback(e);
12279 });
12280 req.end();
12281 }
12282
12283 // options parameter is *REQUIRED* for VM.stop()
12284 exports.stop = function (uuid, options, callback)
12285 {
12286 var load_fields;
12287 var log;
12288 var unset_autoboot = 'set autoboot=false';
12289 var vmobj;
12290
12291 load_fields = [
12292 'brand',
12293 'state',
12294 'uuid',
12295 'zonename'
12296 ];
12297
12298 if (!options) {
12299 options = {};
12300 }
12301
12302 if (options.hasOwnProperty('force') && options.force) {
12303 ensureLogging(true);
12304 if (options.hasOwnProperty('log')) {
12305 log = options.log;
12306 } else {
12307 log = VM.log.child({action: 'stop-F', vm: uuid});
12308 }
12309 kill(uuid, log, callback);
12310 return;
12311 } else {
12312 ensureLogging(true);
12313 if (options.hasOwnProperty('log')) {
12314 log = options.log;
12315 } else {
12316 log = VM.log.child({action: 'stop', vm: uuid});
12317 }
12318 }
12319
12320 log.info('Stopping VM ' + uuid);
12321
12322 if (!options.timeout) {
12323 options.timeout = 180;
12324 }
12325 if (!options.transition_to) {
12326 options.transition_to = 'stopped';
12327 }
12328
12329 async.series([
12330 function (cb) {
12331 /* We load here to ensure this vm exists. */
12332 VM.load(uuid, {log: log, fields: load_fields}, function (err, obj) {
12333 var new_err;
12334
12335 if (err) {
12336 log.error(err);
12337 cb(err);
12338 return;
12339 } else {
12340 vmobj = obj;
12341 if (vmobj.state !== 'running') {
12342 new_err = new Error('VM ' + vmobj.uuid + ' is already '
12343 + 'not \'running\' (currently: ' + vmobj.state
12344 + ')');
12345 new_err.code = 'ENOTRUNNING';
12346 cb(new_err);
12347 } else {
12348 cb();
12349 }
12350 }
12351 });
12352 }, function (cb) {
12353 // When stopping a VM that uses vm_autoboot, we assume we also do
12354 // the stop through vmadmd.
12355 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12356 async.series([
12357 function (callbk) {
12358 setTransition(vmobj, 'stopping', options.transition_to,
12359 (options.timeout * 1000), log, function (err) {
12360
12361 callbk(err);
12362 });
12363 }, function (callbk) {
12364 postVmadmd(vmobj.uuid, 'stop',
12365 {'timeout': options.timeout}, log, function (err) {
12366
12367 if (err) {
12368 log.error(err);
12369 err.message = 'Unable to post "stop" to vmadmd:'
12370 + ' ' + err.message;
12371 }
12372 callbk(err);
12373 });
12374 }, function (callbk) {
12375
12376 // different version for VMs
12377 unset_autoboot = 'select attr name=vm-autoboot; '
12378 + 'set value=false; end';
12379
12380 zonecfg(['-u', uuid, unset_autoboot], log,
12381 function (err, fds) {
12382 if (err) {
12383 // The vm is dead at this point, failing
12384 // here would do no good, so we just log it.
12385 log.error({err: err, stdout: fds.stdout,
12386 stderr: fds.stderr}, 'stop(): Failed to'
12387 + ' ' + unset_autoboot + ' for ' + uuid
12388 + ': ' + err.message);
12389 } else {
12390 log.info({stdout: fds.stdout,
12391 stderr: fds.stderr}, 'Stopped ' + uuid);
12392 }
12393 callbk();
12394 }
12395 );
12396 }
12397 ], function (err) {
12398 cb(err);
12399 });
12400 } else { // no vm_autoboot / vmadmd stop
12401 cb();
12402 }
12403 }, function (cb) {
12404 var args;
12405
12406 // joyent brand specific stuff
12407 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0',
12408 '-i', '5'];
12409
12410 // not using vm_autoboot means using the 'normal' boot process
12411 if (!BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12412 async.series([
12413 function (callbk) {
12414 log.debug('/usr/sbin/zlogin ' + args.join(' '));
12415 execFile('/usr/sbin/zlogin', args,
12416 function (err, stdout, stderr) {
12417
12418 if (err) {
12419 log.error({'err': err, 'stdout': stdout,
12420 'stderr': stderr}, 'zlogin for '
12421 + vmobj.zonename + ' exited with code'
12422 + err.code + ': ' + err.message);
12423 callbk(err);
12424 } else {
12425 log.debug('zlogin claims to have worked, '
12426 + 'stdout:\n' + stdout + '\nstderr:\n'
12427 + stderr);
12428 callbk();
12429 }
12430 });
12431 }, function (callbk) {
12432 zonecfg(['-u', uuid, unset_autoboot], log,
12433 function (err, fds) {
12434 if (err) {
12435 // The vm is dead at this point, failing
12436 // do no good, so we just log it.
12437 log.warn({err: err, stdout: fds.stdout,
12438 stderr: fds.stderr}, 'Failed to '
12439 + unset_autoboot + ' for ' + uuid);
12440 } else {
12441 log.info({stdout: fds.stdout,
12442 stderr: fds.stderr}, 'Stopped ' + uuid);
12443 }
12444 callbk();
12445 }
12446 );
12447 }
12448 ], function (err) {
12449 cb(err);
12450 });
12451 } else { // using vmautoboot so won't shutdown from in the zone
12452 cb();
12453 }
12454 }, function (cb) {
12455 // Verify it's shut down
12456 VM.waitForZoneState(vmobj, 'installed', {log: log},
12457 function (err, result) {
12458
12459 if (err) {
12460 if (err.code === 'ETIMEOUT') {
12461 log.info(err, 'timeout waiting for zone to go to '
12462 + '"installed"');
12463 } else {
12464 log.error(err, 'unknown error waiting for zone to go'
12465 + ' "installed"');
12466 }
12467 cb(err);
12468 } else {
12469 // zone got to stopped
12470 log.info('VM seems to have switched to "installed"');
12471 cb();
12472 }
12473 });
12474 }
12475 ], function (err) {
12476 callback(err);
12477 });
12478 };
12479
12480 // sends several query-* commands to QMP to get details for a VM
12481 exports.info = function (uuid, types, options, callback)
12482 {
12483 var load_fields;
12484 var log;
12485
12486 // options is optional
12487 if (arguments.length === 3) {
12488 callback = arguments[2];
12489 options = {};
12490 }
12491
12492 ensureLogging(false);
12493 if (options.hasOwnProperty('log')) {
12494 log = options.log;
12495 } else {
12496 log = VM.log.child({action: 'info', vm: uuid});
12497 }
12498
12499 load_fields = [
12500 'brand',
12501 'state',
12502 'uuid'
12503 ];
12504
12505 // load to ensure we're a VM
12506 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12507 var type;
12508
12509 if (err) {
12510 callback(err);
12511 return;
12512 }
12513
12514 if (!BRAND_OPTIONS[vmobj.brand].features.runtime_info) {
12515 // XXX if support is added to other brands, update this message.
12516 callback(new Error('the info command is only supported for KVM '
12517 + 'VMs'));
12518 return;
12519 }
12520
12521 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') {
12522 callback(new Error('Unable to get info for vm from state "'
12523 + vmobj.state + '", must be "running" or "stopping".'));
12524 return;
12525 }
12526
12527 if (!types) {
12528 types = ['all'];
12529 }
12530
12531 for (type in types) {
12532 type = types[type];
12533 if (VM.INFO_TYPES.indexOf(type) === -1) {
12534 callback(new Error('unknown info type: ' + type));
12535 return;
12536 }
12537 }
12538
12539 http.get({ host: '127.0.0.1', port: 8080, path: '/vm/' + uuid + '/info'
12540 + '?types=' + types.join(',') }, function (res) {
12541
12542 var data = '';
12543
12544 if (res.statusCode !== 200) {
12545 callback(new Error('Unable to get info from vmadmd, query '
12546 + 'returned ' + res.statusCode + '.'));
12547 } else {
12548 res.on('data', function (d) {
12549 data = data + d.toString();
12550 });
12551 res.on('end', function (d) {
12552 callback(null, JSON.parse(data));
12553 });
12554 }
12555 }
12556 ).on('error', function (e) {
12557 log.error(e);
12558 callback(e);
12559 });
12560 });
12561 };
12562
12563 function reset(uuid, log, callback)
12564 {
12565 var load_fields;
12566
12567 assert(log, 'no logger passed to reset()');
12568
12569 log.info('Resetting VM ' + uuid);
12570
12571 load_fields = [
12572 'brand',
12573 'state',
12574 'uuid'
12575 ];
12576
12577 /* We load here to ensure this vm exists. */
12578 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12579 if (err) {
12580 callback(err);
12581 return;
12582 }
12583
12584 if (vmobj.state !== 'running') {
12585 callback(new Error('Cannot reset vm from state "'
12586 + vmobj.state + '", must be "running".'));
12587 return;
12588 }
12589
12590 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) {
12591 postVmadmd(vmobj.uuid, 'reset', {}, log, function (e) {
12592 if (e) {
12593 callback(new Error('Unable to post "reset" to '
12594 + 'vmadmd: ' + e.message));
12595 } else {
12596 callback();
12597 }
12598 });
12599 } else {
12600 zoneadm(['-u', vmobj.uuid, 'reboot', '-X'], log, function (e, fds) {
12601 if (e) {
12602 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
12603 'zoneadm failed to reboot VM ' + vmobj.uuid);
12604 callback(new Error(rtrim(fds.stderr)));
12605 } else {
12606 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12607 'zoneadm rebooted VM ' + vmobj.uuid);
12608 callback();
12609 }
12610 });
12611 }
12612 });
12613 }
12614
12615 // options is *REQUIRED* for VM.reboot()
12616 exports.reboot = function (uuid, options, callback)
12617 {
12618 var cleanup;
12619 var log;
12620 var reboot_async = false;
12621 var reboot_complete = false;
12622 var vmobj;
12623
12624 if (options.hasOwnProperty('log')) {
12625 log = options.log;
12626 }
12627
12628 if (options.hasOwnProperty('force') && options.force) {
12629 ensureLogging(true);
12630 if (!log) {
12631 log = VM.log.child({action: 'reboot-F', vm: uuid});
12632 }
12633 reset(uuid, log, callback);
12634 return;
12635 } else {
12636 ensureLogging(true);
12637 log = VM.log.child({action: 'reboot', vm: uuid});
12638 }
12639
12640 log.info('Rebooting VM ' + uuid);
12641
12642 if (!options) {
12643 options = {};
12644 }
12645
12646 async.series([
12647 function (cb) {
12648 var load_fields = [
12649 'brand',
12650 'nics',
12651 'state',
12652 'zonename'
12653 ];
12654
12655 VM.load(uuid, {fields: load_fields, log: log},
12656 function (err, obj) {
12657
12658 if (err) {
12659 cb(err);
12660 return;
12661 }
12662
12663 if (obj.state !== 'running') {
12664 cb(new Error('Cannot reboot vm from state "' + obj.state
12665 + '", must be "running"'));
12666 return;
12667 }
12668
12669 vmobj = obj;
12670 cb();
12671 });
12672 }, function (cb) {
12673 // If nic tags have disappeared out from under us, don't allow a
12674 // reboot that will put us into a bad state
12675 lookupInvalidNicTags(vmobj.nics, log, function (e) {
12676 if (e) {
12677 cb(new Error('Cannot reboot vm: ' + e.message));
12678 return;
12679 }
12680
12681 cb();
12682 });
12683
12684 }, function (cb) {
12685 var watcherobj;
12686
12687 if (!reboot_async) {
12688 watcherobj = watchZoneTransitions(function (err, ze) {
12689 if (!err && ze.zonename !== vmobj.zonename) {
12690 // not something we need to handle
12691 return;
12692 }
12693
12694 if (err) {
12695 // XXX what should we do here?
12696 log.error(err);
12697 return;
12698 }
12699
12700 log.debug(ze); // TODO move to trace
12701
12702 if (ze.newstate === 'running'
12703 && ze.oldstate !== 'running') {
12704
12705 if (watcherobj) {
12706 // cleanup our watcher since we found what we're
12707 // looking for.
12708 cleanup();
12709 }
12710
12711 reboot_complete = true;
12712 }
12713 }, log);
12714 cleanup = watcherobj.cleanup;
12715 }
12716
12717 cb();
12718 }, function (cb) {
12719 var args;
12720
12721 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) {
12722 // here we stop the machine and set a transition so vmadmd will
12723 // start the machine once the stop finished.
12724 options.transition_to = 'start';
12725 options.log = log;
12726 VM.stop(uuid, options, function (err) {
12727 if (err) {
12728 cb(err);
12729 } else {
12730 cb();
12731 }
12732 });
12733 } else {
12734 // joyent branded zones
12735 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0',
12736 '-i', '6'];
12737 log.debug('/usr/sbin/zlogin ' + args.join(' '));
12738 execFile('/usr/sbin/zlogin', args,
12739 function (err, stdout, stderr) {
12740 if (err) {
12741 log.error({'err': err, 'stdout': stdout,
12742 'stderr': stderr}, 'zlogin for ' + vmobj.zonename
12743 + ' exited with code' + err.code + ': '
12744 + err.message);
12745 cb(err);
12746 } else {
12747 cb();
12748 }
12749 });
12750 }
12751 }, function (cb) {
12752 var ival;
12753 var ticks = 0;
12754
12755 if (reboot_async) {
12756 cb();
12757 return;
12758 } else {
12759 ticks = 180 * 10; // (180 * 10) 100ms ticks = 3m
12760 ival = setInterval(function () {
12761 if (reboot_complete) {
12762 log.debug('reboot marked complete, cleaning up');
12763 clearInterval(ival);
12764 cleanup();
12765 cb();
12766 return;
12767 }
12768 ticks--;
12769 if (ticks <= 0) {
12770 // timed out
12771 log.debug('reboot timed out, cleaning up');
12772 clearInterval(ival);
12773 cleanup();
12774 cb(new Error('timed out waiting for zone to reboot'));
12775 return;
12776 }
12777 }, 100);
12778 }
12779 }
12780 ], function (err) {
12781 callback(err);
12782 });
12783 };
12784
12785 // options is *REQUIRED* for VM.sysrq
12786 exports.sysrq = function (uuid, req, options, callback)
12787 {
12788 var load_fields;
12789 var log;
12790
12791 ensureLogging(true);
12792
12793 if (options.hasOwnProperty('log')) {
12794 log = options.log;
12795 } else {
12796 log = VM.log.child({action: 'sysrq-' + req, vm: uuid});
12797 }
12798
12799 log.info('Sending sysrq "' + req + '" to ' + uuid);
12800
12801 load_fields = [
12802 'brand',
12803 'state',
12804 'uuid'
12805 ];
12806
12807 /* We load here to ensure this vm exists. */
12808 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12809 if (err) {
12810 callback(err);
12811 return;
12812 }
12813
12814 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') {
12815 callback(new Error('Unable to send request to vm from state "'
12816 + vmobj.state + '", must be "running" or "stopping".'));
12817 return;
12818 }
12819
12820 if (BRAND_OPTIONS[vmobj.brand].features.type !== 'KVM') {
12821 callback(new Error('The sysrq command is only supported for KVM.'));
12822 return;
12823 }
12824
12825 if (VM.SYSRQ_TYPES.indexOf(req) === -1) {
12826 callback(new Error('Invalid sysrq "' + req + '" valid values: '
12827 + '"' + VM.SYSRQ_TYPES.join('","') + '".'));
12828 return;
12829 }
12830
12831 postVmadmd(vmobj.uuid, 'sysrq', {'request': req}, log, function (e) {
12832 if (e) {
12833 callback(new Error('Unable to post "sysrq" to vmadmd: '
12834 + e.message));
12835 } else {
12836 callback();
12837 }
12838 });
12839 });
12840 };
12841
12842 exports.console = function (uuid, options, callback)
12843 {
12844 var load_fields;
12845 var log;
12846
12847 // options is optional
12848 if (arguments.length === 2) {
12849 callback = arguments[1];
12850 options = {};
12851 }
12852
12853 ensureLogging(false);
12854 if (options.hasOwnProperty('log')) {
12855 log = options.log;
12856 } else {
12857 log = VM.log.child({action: 'console', vm: uuid});
12858 }
12859
12860 load_fields = [
12861 'brand',
12862 'state',
12863 'zonename',
12864 'zonepath'
12865 ];
12866
12867 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12868 var args;
12869 var child;
12870 var cmd;
12871 var stty;
12872
12873 if (err) {
12874 callback(err);
12875 return;
12876 }
12877 if (vmobj.state !== 'running') {
12878 callback(new Error('cannot connect to console when state is '
12879 + '"' + vmobj.state + '" must be "running".'));
12880 return;
12881 }
12882
12883 if (BRAND_OPTIONS[vmobj.brand].features.zlogin_console) {
12884 cmd = '/usr/sbin/zlogin';
12885 args = ['-C', '-e', '\\035', vmobj.zonename];
12886
12887 log.debug(cmd + ' ' + args.join(' '));
12888 child = spawn(cmd, args, {customFds: [0, 1, 2]});
12889 child.on('close', function (code) {
12890 log.debug('zlogin process exited with code ' + code);
12891 callback();
12892 });
12893 } else if (BRAND_OPTIONS[vmobj.brand].features.serial_console) {
12894 async.series([
12895 function (cb) {
12896 cmd = '/usr/bin/stty';
12897 args = ['-g'];
12898 stty = '';
12899
12900 log.debug(cmd + ' ' + args.join(' '));
12901 child = spawn(cmd, args, {customFds: [0, -1, -1]});
12902 child.stdout.on('data', function (data) {
12903 // log.debug('data: ' + data.toString());
12904 stty = data.toString();
12905 });
12906 child.on('close', function (code) {
12907 log.debug('stty process exited with code ' + code);
12908 cb();
12909 });
12910 }, function (cb) {
12911 cmd = '/usr/bin/socat';
12912 args = ['unix-client:' + vmobj.zonepath
12913 + '/root/tmp/vm.console', '-,raw,echo=0,escape=0x1d'];
12914
12915 log.debug(cmd + ' ' + args.join(' '));
12916 child = spawn(cmd, args, {customFds: [0, 1, 2]});
12917 child.on('close', function (code) {
12918 log.debug('zlogin process exited with code ' + code);
12919 cb();
12920 });
12921 }, function (cb) {
12922 cmd = '/usr/bin/stty';
12923 args = [stty];
12924
12925 log.debug(cmd + ' ' + args.join(' '));
12926 child = spawn(cmd, args, {customFds: [0, -1, -1]});
12927 child.on('close', function (code) {
12928 log.debug('stty process exited with code ' + code);
12929 cb();
12930 });
12931 }
12932 ], function (e, results) {
12933 callback(e);
12934 });
12935 } else {
12936 callback(new Error('Cannot get console for brand: ' + vmobj.brand));
12937 }
12938 });
12939 };